Compare commits
14 Commits
7cdab010b3
...
pre-split-
| Author | SHA1 | Date | |
|---|---|---|---|
| 4470fcee0a | |||
| 39e308d9ae | |||
| 8e6e72a8b9 | |||
| 226b44053c | |||
| 0493a4ddc7 | |||
| c4c20c6d0a | |||
| 4a494a101d | |||
| ca7e30aa05 | |||
| 983e06b53d | |||
| 53b65dc30e | |||
| f545438fa8 | |||
| 3b622d3d17 | |||
| aa747dcc85 | |||
| 56c3406f25 |
@@ -28,4 +28,4 @@ plymouth quit
|
|||||||
|
|
||||||
# Start X in /home/pi
|
# Start X in /home/pi
|
||||||
cd /home/pi
|
cd /home/pi
|
||||||
sudo -u pi startx
|
sudo -u pi startx -- -nocursor
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ plymouth quit 2>/dev/null || true
|
|||||||
# late-boot units (bbctrl logrotate, etc.) don't block on it. Output
|
# late-boot units (bbctrl logrotate, etc.) don't block on it. Output
|
||||||
# is redirected so the journal doesn't fill up with X warnings.
|
# is redirected so the journal doesn't fill up with X warnings.
|
||||||
cd /home/pi
|
cd /home/pi
|
||||||
nohup sudo -u pi startx >/var/log/onefin-x.log 2>&1 &
|
# `-- -nocursor` hides the X pointer; this is a touchscreen kiosk and
|
||||||
|
# the mouse cursor only gets in the way.
|
||||||
|
nohup sudo -u pi startx -- -nocursor >/var/log/onefin-x.log 2>&1 &
|
||||||
disown
|
disown
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ sed -i 's/^PARTUUID=.*\//\/dev\/mmcblk0p2 \//' /etc/fstab
|
|||||||
|
|
||||||
# Enable browser in xorg
|
# Enable browser in xorg
|
||||||
sed -i 's/allowed_users=console/allowed_users=anybody/' /etc/X11/Xwrapper.config
|
sed -i 's/allowed_users=console/allowed_users=anybody/' /etc/X11/Xwrapper.config
|
||||||
echo "sudo -u pi startx" >> /etc/rc.local
|
echo "sudo -u pi startx -- -nocursor" >> /etc/rc.local
|
||||||
cp /mnt/host/xinitrc /home/pi/.xinitrc
|
cp /mnt/host/xinitrc /home/pi/.xinitrc
|
||||||
cp /mnt/host/ratpoisonrc /home/pi/.ratpoisonrc
|
cp /mnt/host/ratpoisonrc /home/pi/.ratpoisonrc
|
||||||
cp /mnt/host/xorg.conf /etc/X11/
|
cp /mnt/host/xorg.conf /etc/X11/
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
// V09 W-axis page \u2014 mounts the existing WAxisSettings Svelte component
|
// V09 A-axis page — mounts the AAxisSettings Svelte component
|
||||||
// inside the settings shell so it gets a real top-level rail entry
|
// inside the settings shell so it gets a real top-level rail entry
|
||||||
// instead of being a soft-link anchor inside Display & Units.
|
// instead of being a soft-link anchor inside Display & Units.
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
template: "#w-axis-view-template",
|
template: "#a-axis-view-template",
|
||||||
|
|
||||||
attached: function () {
|
attached: function () {
|
||||||
this.svelteComponent = SvelteComponents.createComponent(
|
this.svelteComponent = SvelteComponents.createComponent(
|
||||||
"WAxisSettings",
|
"AAxisSettings",
|
||||||
document.getElementById("w-axis-mount")
|
document.getElementById("a-axis-mount")
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ 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
|
// Embedded Svelte subviews (A axis settings, etc.) signal
|
||||||
// unsaved changes via this event. The master Save button
|
// unsaved changes via this event. The master Save button
|
||||||
// highlights when modified is true.
|
// highlights when modified is true.
|
||||||
window.addEventListener("onefin:dirty", () => {
|
window.addEventListener("onefin:dirty", () => {
|
||||||
@@ -391,7 +391,7 @@ module.exports = new Vue({
|
|||||||
"admin-general", "admin-network",
|
"admin-general", "admin-network",
|
||||||
"motor", "tool", "io", "macros",
|
"motor", "tool", "io", "macros",
|
||||||
"help", "cheat-sheet",
|
"help", "cheat-sheet",
|
||||||
"w-axis",
|
"a-axis",
|
||||||
];
|
];
|
||||||
const initialHead = (location.hash || "").replace(/^#/, "").split(":")[0];
|
const initialHead = (location.hash || "").replace(/^#/, "").split(":")[0];
|
||||||
if (settingsFamily.indexOf(initialHead) === -1) {
|
if (settingsFamily.indexOf(initialHead) === -1) {
|
||||||
@@ -627,7 +627,7 @@ module.exports = new Vue({
|
|||||||
"admin-general", "admin-network",
|
"admin-general", "admin-network",
|
||||||
"motor", "tool", "io", "macros",
|
"motor", "tool", "io", "macros",
|
||||||
"help", "cheat-sheet",
|
"help", "cheat-sheet",
|
||||||
"w-axis",
|
"a-axis",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (head == "control") {
|
if (head == "control") {
|
||||||
@@ -690,7 +690,7 @@ module.exports = new Vue({
|
|||||||
try {
|
try {
|
||||||
await api.put("config/save", this.config);
|
await api.put("config/save", this.config);
|
||||||
// Notify any embedded Svelte subviews that own their
|
// Notify any embedded Svelte subviews that own their
|
||||||
// own persistence (W axis -> aux.json, etc.) that
|
// own persistence (A axis -> aux.json, etc.) that
|
||||||
// the user just hit the master Save button. They
|
// the user just hit the master Save button. They
|
||||||
// listen for `onefin:save-all` and PUT their state.
|
// listen for `onefin:save-all` and PUT their state.
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -256,10 +256,11 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_compute_aux_axis: function() {
|
_compute_aux_axis: function() {
|
||||||
// Virtual W axis driven by the auxcnc ESP32. Position, homed
|
// Auxiliary axis driven by the auxcnc ESP32 (typically
|
||||||
// flag and presence come from the bbctrl AuxAxis driver via
|
// exposed to gplan as A). Position, homed flag and
|
||||||
// state.aux_*. No motor mapping, no soft-limit warnings on
|
// presence come from the bbctrl AuxAxis driver via
|
||||||
// toolpath bounds (auxcnc enforces its own).
|
// state.aux_*. No motor mapping, no soft-limit warnings
|
||||||
|
// on toolpath bounds (auxcnc enforces its own).
|
||||||
const enabled = !!this.state.aux_enabled;
|
const enabled = !!this.state.aux_enabled;
|
||||||
const present = !!this.state.aux_present;
|
const present = !!this.state.aux_present;
|
||||||
const homed = !!this.state.aux_homed;
|
const homed = !!this.state.aux_homed;
|
||||||
@@ -269,12 +270,12 @@ module.exports = {
|
|||||||
let state = present ? "UNHOMED" : "OFFLINE";
|
let state = present ? "UNHOMED" : "OFFLINE";
|
||||||
let icon = present ? "question-circle" : "plug";
|
let icon = present ? "question-circle" : "plug";
|
||||||
let title = present
|
let title = present
|
||||||
? "Click the home button to home W axis."
|
? "Click the home button to home the auxiliary axis."
|
||||||
: "Aux controller not connected on /dev/ttyUSB0.";
|
: "Aux controller not connected on /dev/ttyUSB0.";
|
||||||
if (homed) {
|
if (homed) {
|
||||||
state = "HOMED";
|
state = "HOMED";
|
||||||
icon = "check-circle";
|
icon = "check-circle";
|
||||||
title = "W axis successfully homed.";
|
title = "Auxiliary axis successfully homed.";
|
||||||
} else if (!present) {
|
} else if (!present) {
|
||||||
klass += " error";
|
klass += " error";
|
||||||
}
|
}
|
||||||
@@ -295,7 +296,7 @@ module.exports = {
|
|||||||
title: title,
|
title: title,
|
||||||
ticon: "check-circle",
|
ticon: "check-circle",
|
||||||
tstate: "OK",
|
tstate: "OK",
|
||||||
toolmsg: "W axis is not constrained by tool path bounds.",
|
toolmsg: "Auxiliary axis is not constrained by tool path bounds.",
|
||||||
tklass: `${homed ? "homed" : "unhomed"} axis-w`,
|
tklass: `${homed ? "homed" : "unhomed"} axis-w`,
|
||||||
isAux: true,
|
isAux: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -251,19 +251,24 @@ module.exports = {
|
|||||||
|
|
||||||
aux_home: function () {
|
aux_home: function () {
|
||||||
api.put("aux/home").catch(function (err) {
|
api.put("aux/home").catch(function (err) {
|
||||||
console.error("W home failed:", err);
|
console.error("Aux home failed:", err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Home every enabled axis (legacy Onefinity "Home All"). Sequence:
|
// 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
|
// 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
|
// 2. Auxiliary axis via /api/aux/home on the ESP
|
||||||
|
// ONLY when the auxcnc axis is not integrated as a virtual
|
||||||
|
// machine axis. With the gplan A-axis integration (synthetic
|
||||||
|
// motor 4 enabled), Mach.home() already homes the external
|
||||||
|
// axis as part of the xyzabc pass - calling aux/home
|
||||||
|
// afterwards would home it a second time.
|
||||||
// /api/home returns as soon as the request is queued, not when
|
// /api/home returns as soon as the request is queued, not when
|
||||||
// homing completes, so we have to watch state.cycle:
|
// homing completes, so we have to watch state.cycle:
|
||||||
// - first wait for it to *leave* 'idle' (cycle began),
|
// - first wait for it to *leave* 'idle' (cycle began),
|
||||||
// - then wait for it to come *back* to 'idle' (cycle ended).
|
// - 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
|
// Only then do we fire the auxiliary home, so the gantry and the
|
||||||
// ESP never move at the same time.
|
// auxcnc ESP never move at the same time.
|
||||||
home_all: async function () {
|
home_all: async function () {
|
||||||
this.ask_home = false;
|
this.ask_home = false;
|
||||||
try {
|
try {
|
||||||
@@ -274,19 +279,24 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
if (!this.w || !this.w.enabled) return;
|
if (!this.w || !this.w.enabled) return;
|
||||||
|
|
||||||
|
// When the synthetic external motor (index 4) is enabled,
|
||||||
|
// the auxcnc axis is mapped onto a real machine axis letter
|
||||||
|
// (e.g. A) and was already homed by /api/home above.
|
||||||
|
if (this.state && this.state["4me"]) return;
|
||||||
|
|
||||||
const wait = (ms) => new Promise(r => setTimeout(r, ms));
|
const wait = (ms) => new Promise(r => setTimeout(r, ms));
|
||||||
const cycleNow = () => (this.state && this.state.cycle) || "idle";
|
const cycleNow = () => (this.state && this.state.cycle) || "idle";
|
||||||
|
|
||||||
// Phase 1: wait up to 5s for the homing cycle to actually start.
|
// Phase 1: wait up to 5s for the homing cycle to actually start.
|
||||||
// If the request was rejected upstream (e.g. estopped) cycle
|
// If the request was rejected upstream (e.g. estopped) cycle
|
||||||
// never leaves idle and we bail rather than home W in isolation.
|
// never leaves idle and we bail rather than home A in isolation.
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
while (Date.now() - startedAt < 5000) {
|
while (Date.now() - startedAt < 5000) {
|
||||||
if (cycleNow() != "idle") break;
|
if (cycleNow() != "idle") break;
|
||||||
await wait(100);
|
await wait(100);
|
||||||
}
|
}
|
||||||
if (cycleNow() == "idle") {
|
if (cycleNow() == "idle") {
|
||||||
console.warn("home_all: main homing cycle never started; skipping W");
|
console.warn("home_all: main homing cycle never started; skipping aux");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,13 +312,13 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
api.put("aux/home").catch(function (err) {
|
api.put("aux/home").catch(function (err) {
|
||||||
console.error("W home failed:", err);
|
console.error("Aux 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("Aux jog failed:", err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ module.exports = {
|
|||||||
"io-view": require("./io-view"),
|
"io-view": require("./io-view"),
|
||||||
"macros-view": require("./macros"),
|
"macros-view": require("./macros"),
|
||||||
"help-view": require("./help-view"),
|
"help-view": require("./help-view"),
|
||||||
"w-axis-view": require("./w-axis-view"),
|
"a-axis-view": require("./a-axis-view"),
|
||||||
"cheat-sheet-view": {
|
"cheat-sheet-view": {
|
||||||
template: "#cheat-sheet-view-template",
|
template: "#cheat-sheet-view-template",
|
||||||
data: function () {
|
data: function () {
|
||||||
@@ -57,9 +57,9 @@ module.exports = {
|
|||||||
{ sub: "motor", motor: 1, href: "#motor:1", icon: "fa-arrows-up-down-left-right", label: "Motor 1" },
|
{ sub: "motor", motor: 1, href: "#motor:1", icon: "fa-arrows-up-down-left-right", label: "Motor 1" },
|
||||||
{ sub: "motor", motor: 2, href: "#motor:2", icon: "fa-arrows-up-down-left-right", label: "Motor 2" },
|
{ sub: "motor", motor: 2, href: "#motor:2", icon: "fa-arrows-up-down-left-right", label: "Motor 2" },
|
||||||
{ sub: "motor", motor: 3, href: "#motor:3", icon: "fa-arrows-up-down-left-right", label: "Motor 3" },
|
{ sub: "motor", motor: 3, href: "#motor:3", icon: "fa-arrows-up-down-left-right", label: "Motor 3" },
|
||||||
// W axis is auxiliary (auxcnc ESP32). It mounts the existing
|
// Auxiliary axis (auxcnc ESP32 - exposed to gplan as A).
|
||||||
// WAxisSettings Svelte component on its own page.
|
// Mounts the AAxisSettings Svelte component on its own page.
|
||||||
{ sub: "w-axis", href: "#w-axis", icon: "fa-arrows-up-down", label: "W Axis" },
|
{ sub: "a-axis", href: "#a-axis", icon: "fa-arrows-up-down", label: "A Axis" },
|
||||||
{ section: " " },
|
{ section: " " },
|
||||||
{ sub: "help", href: "#help", icon: "fa-circle-question", label: "Help" },
|
{ sub: "help", href: "#help", icon: "fa-circle-question", label: "Help" },
|
||||||
],
|
],
|
||||||
@@ -137,7 +137,7 @@ module.exports = {
|
|||||||
// layout, which under tablet mode pulls the fixed header out
|
// layout, which under tablet mode pulls the fixed header out
|
||||||
// of view.
|
// of view.
|
||||||
if (location.hash !== item.href) location.hash = item.href;
|
if (location.hash !== item.href) location.hash = item.href;
|
||||||
this._w_axis_focus = (item.sub === "w-axis");
|
this._a_axis_focus = (item.sub === "a-axis");
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
// Force any inadvertent ancestor scroll back to 0 before
|
// Force any inadvertent ancestor scroll back to 0 before
|
||||||
// we move .settings-content explicitly.
|
// we move .settings-content explicitly.
|
||||||
@@ -160,7 +160,7 @@ module.exports = {
|
|||||||
requestAnimationFrame(reset);
|
requestAnimationFrame(reset);
|
||||||
}, 320);
|
}, 320);
|
||||||
} else {
|
} else {
|
||||||
this._w_axis_focus = false;
|
this._a_axis_focus = false;
|
||||||
if (location.hash !== item.href) location.hash = item.href;
|
if (location.hash !== item.href) location.hash = item.href;
|
||||||
// Reset .app-body scroll so each route starts at the top.
|
// Reset .app-body scroll so each route starts at the top.
|
||||||
const body = document.querySelector(".app-body");
|
const body = document.querySelector(".app-body");
|
||||||
|
|||||||
4
src/pug/templates/a-axis-view.pug
Normal file
4
src/pug/templates/a-axis-view.pug
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
script#a-axis-view-template(type="text/x-template")
|
||||||
|
#a-axis-page
|
||||||
|
h1 A Axis (auxcnc)
|
||||||
|
#a-axis-mount
|
||||||
@@ -92,17 +92,21 @@ script#control-view-template(type="text/x-template")
|
|||||||
.fa.fa-arrow-down.ico(style="transform: rotate(-45deg)")
|
.fa.fa-arrow-down.ico(style="transform: rotate(-45deg)")
|
||||||
button.jbtn(@click="jog_fn(0, 0, -1, 0)") Z−
|
button.jbtn(@click="jog_fn(0, 0, -1, 0)") Z−
|
||||||
|
|
||||||
// Row 4 — W axis (auxcnc) when enabled.
|
// Row 4 — A axis (the auxcnc-driven external axis) when enabled.
|
||||||
// W- | W+ | Probe XYZ | Probe Z
|
// A- | A+ | Probe XYZ | Probe Z
|
||||||
// "Home W" lives in the DRO table's actions column on the
|
// "Home A" lives in the DRO table's actions column on the
|
||||||
// right, so it doesn't need a tile here.
|
// right, so it doesn't need a tile here. The legacy w.enabled
|
||||||
template(v-if="w.enabled")
|
// gate is kept so older installs (where the auxcnc axis still
|
||||||
button.jbtn(@click="aux_jog_incr(-1)", :disabled="!w.enabled")
|
// appears as W via the side-channel) keep working.
|
||||||
|
template(v-if="w.enabled || a.enabled")
|
||||||
|
button.jbtn(@click="aux_jog_incr(-1)",
|
||||||
|
:disabled="!(w.enabled || a.enabled)")
|
||||||
.fa.fa-arrow-down.ico
|
.fa.fa-arrow-down.ico
|
||||||
span.lbl W−
|
span.lbl A−
|
||||||
button.jbtn(@click="aux_jog_incr(+1)", :disabled="!w.enabled")
|
button.jbtn(@click="aux_jog_incr(+1)",
|
||||||
|
:disabled="!(w.enabled || a.enabled)")
|
||||||
.fa.fa-arrow-up.ico
|
.fa.fa-arrow-up.ico
|
||||||
span.lbl W+
|
span.lbl A+
|
||||||
button.jbtn(@click="showProbeDialog('xyz')",
|
button.jbtn(@click="showProbeDialog('xyz')",
|
||||||
:class="{'load-on': !state['pw']}")
|
:class="{'load-on': !state['pw']}")
|
||||||
.fa.fa-bullseye.ico
|
.fa.fa-bullseye.ico
|
||||||
@@ -211,13 +215,11 @@ script#control-view-template(type="text/x-template")
|
|||||||
div Position
|
div Position
|
||||||
div Absolute
|
div Absolute
|
||||||
div Offset
|
div Offset
|
||||||
div State
|
|
||||||
div Toolpath
|
|
||||||
.actions-cell
|
.actions-cell
|
||||||
// Master Home All. Each row's Actions cell has a per-axis
|
// Master Home All. Each row's Actions cell has a per-axis
|
||||||
// home button; this header-level button homes every
|
// home button; this header-level button homes every
|
||||||
// enabled axis (legacy Onefinity behavior). Auto-includes
|
// enabled axis (legacy Onefinity behavior). Auto-includes
|
||||||
// the W axis when it is enabled.
|
// the auxiliary A axis when it is enabled.
|
||||||
button.icon-btn(:disabled="!is_idle",
|
button.icon-btn(:disabled="!is_idle",
|
||||||
title="Home all axes.", @click="home_all()")
|
title="Home all axes.", @click="home_all()")
|
||||||
.fa.fa-house-chimney
|
.fa.fa-house-chimney
|
||||||
@@ -226,39 +228,32 @@ script#control-view-template(type="text/x-template")
|
|||||||
each axis in 'xyzabc'
|
each axis in 'xyzabc'
|
||||||
.dro-row(:class=`${axis}.klass + ' ' + ${axis}.tklass`,
|
.dro-row(:class=`${axis}.klass + ' ' + ${axis}.tklass`,
|
||||||
v-if=`${axis}.enabled`,
|
v-if=`${axis}.enabled`,
|
||||||
:title=`${axis}.title`)
|
:title=`${axis}.toolmsg ? (${axis}.title + ' — ' + ${axis}.toolmsg) : ${axis}.title`)
|
||||||
.dro-axis(:class=`'axis-' + '${axis}'`)= axis.toUpperCase()
|
.dro-axis(:class=`'axis-' + '${axis}'`)= axis.toUpperCase()
|
||||||
.dro-pos: unit-value(:value=`${axis}.pos`, precision=4)
|
.dro-pos: unit-value(:value=`${axis}.pos`, precision=4)
|
||||||
.dro-sec: unit-value(:value=`${axis}.abs`, precision=3)
|
.dro-sec: unit-value(:value=`${axis}.abs`, precision=3)
|
||||||
.dro-sec: unit-value(:value=`${axis}.off`, precision=3)
|
.dro-sec: unit-value(:value=`${axis}.off`, precision=3)
|
||||||
.dro-state
|
|
||||||
span.chip(:class=`${axis}.tklass.indexOf('error') !== -1 ? 'chip-red' : (${axis}.homed ? 'chip-green' : 'chip-amber')`)
|
|
||||||
.fa(:class=`'fa-' + ${axis}.icon`)
|
|
||||||
| {{#{axis}.state}}
|
|
||||||
.dro-toolpath
|
|
||||||
span.chip(:class=`${axis}.tklass.indexOf('error') !== -1 ? 'chip-red' : (${axis}.tklass.indexOf('warn') !== -1 ? 'chip-amber' : 'chip-green')`,
|
|
||||||
@click=`showToolpathMessageDialog('${axis}')`)
|
|
||||||
.fa(:class=`'fa-' + ${axis}.ticon`)
|
|
||||||
| {{#{axis}.tstate}}
|
|
||||||
.actions-cell
|
.actions-cell
|
||||||
button.icon-btn(:disabled="!can_set_axis",
|
button.icon-btn(:disabled="!can_set_axis",
|
||||||
:title=`'Set ${axis.toUpperCase()} axis position.'`,
|
:title=`'Set ${axis.toUpperCase()} axis position.'`,
|
||||||
@click=`show_set_position('${axis}')`)
|
@click=`show_set_position('${axis}')`)
|
||||||
.fa.fa-gear
|
.fa.fa-gear
|
||||||
button.icon-btn(:disabled="!can_set_axis",
|
button.icon-btn(:class=`${axis}.tklass.indexOf('error') !== -1 ? 'state-red' : (${axis}.tklass.indexOf('warn') !== -1 ? 'state-amber' : 'state-green')`,
|
||||||
:title=`'Zero ${axis.toUpperCase()} axis offset.'`,
|
:disabled="!can_set_axis",
|
||||||
|
:title=`${axis}.toolmsg || ('Zero ${axis.toUpperCase()} axis offset.')`,
|
||||||
@click=`zero('${axis}')`)
|
@click=`zero('${axis}')`)
|
||||||
.fa.fa-location-dot
|
.fa.fa-location-dot
|
||||||
button.icon-btn(:disabled="!is_idle",
|
button.icon-btn(:class=`${axis}.klass.indexOf('error') !== -1 ? 'state-red' : (${axis}.homed ? 'state-green' : 'state-amber')`,
|
||||||
:title=`'Home ${axis.toUpperCase()} axis.'`,
|
:disabled="!is_idle",
|
||||||
|
:title=`${axis}.title`,
|
||||||
@click=`home('${axis}')`)
|
@click=`home('${axis}')`)
|
||||||
.fa.fa-home
|
.fa.fa-home
|
||||||
|
|
||||||
// Legacy W axis row - shown only when the auxcnc stepper is
|
// Legacy auxiliary-axis row - shown only when the auxcnc stepper is
|
||||||
// *not* exposed as a virtual A axis. After v2 the standard
|
// *not* exposed as a virtual A axis. After v2 the standard
|
||||||
// A row above renders this axis natively (with full offset
|
// A row above renders this axis natively (with full offset
|
||||||
// + set-position support); the W row stays for backwards
|
// + set-position support); this row only appears on legacy
|
||||||
// compatibility with installs that haven't migrated.
|
// installs that haven't migrated yet.
|
||||||
.dro-row(:class="w.klass + ' ' + w.tklass",
|
.dro-row(:class="w.klass + ' ' + w.tklass",
|
||||||
v-if="w.enabled && !a.enabled",
|
v-if="w.enabled && !a.enabled",
|
||||||
:title="w.title")
|
:title="w.title")
|
||||||
@@ -266,21 +261,14 @@ script#control-view-template(type="text/x-template")
|
|||||||
.dro-pos: unit-value(:value="w.pos", precision=4)
|
.dro-pos: unit-value(:value="w.pos", precision=4)
|
||||||
.dro-sec: unit-value(:value="w.abs", precision=3)
|
.dro-sec: unit-value(:value="w.abs", precision=3)
|
||||||
.dro-sec —
|
.dro-sec —
|
||||||
.dro-state
|
|
||||||
span.chip(:class="w.homed ? 'chip-green' : 'chip-amber'")
|
|
||||||
.fa(:class="'fa-' + w.icon")
|
|
||||||
| {{w.state}}
|
|
||||||
.dro-toolpath
|
|
||||||
span.chip.chip-green
|
|
||||||
.fa(:class="'fa-' + w.ticon")
|
|
||||||
| {{w.tstate}}
|
|
||||||
.actions-cell
|
.actions-cell
|
||||||
button.icon-btn(disabled, style="visibility:hidden")
|
button.icon-btn(disabled, style="visibility:hidden")
|
||||||
.fa.fa-gear
|
.fa.fa-gear
|
||||||
button.icon-btn(disabled, style="visibility:hidden")
|
button.icon-btn(disabled, style="visibility:hidden")
|
||||||
.fa.fa-location-dot
|
.fa.fa-location-dot
|
||||||
button.icon-btn(:disabled="!w.enabled",
|
button.icon-btn(:class="w.homed ? 'state-green' : 'state-amber'",
|
||||||
title="Home W axis.", @click="aux_home()")
|
:disabled="!w.enabled",
|
||||||
|
title="Home auxiliary axis.", @click="aux_home()")
|
||||||
.fa.fa-home
|
.fa.fa-home
|
||||||
|
|
||||||
// ----- Status strip -----
|
// ----- Status strip -----
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ script#settings-shell-view-template(type="text/x-template")
|
|||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
io-view(v-if="sub === 'io' && config_ready",
|
io-view(v-if="sub === 'io' && config_ready",
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
w-axis-view(v-if="sub === 'w-axis' && config_ready",
|
a-axis-view(v-if="sub === 'a-axis' && config_ready",
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
macros-view(v-if="sub === 'macros' && config_ready",
|
macros-view(v-if="sub === 'macros' && config_ready",
|
||||||
:index="index", :config="config", :template="template", :state="state")
|
:index="index", :config="config", :template="template", :state="state")
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
script#w-axis-view-template(type="text/x-template")
|
|
||||||
#w-axis-page
|
|
||||||
h1 W Axis (auxcnc)
|
|
||||||
#w-axis-mount
|
|
||||||
@@ -40,8 +40,8 @@ DEFAULTS = {
|
|||||||
# 4th axis). gcode uses A for moves; the host ExternalAxis layer
|
# 4th axis). gcode uses A for moves; the host ExternalAxis layer
|
||||||
# forks A motion to the ESP transparently.
|
# forks A motion to the ESP transparently.
|
||||||
'axis_letter': 'a',
|
'axis_letter': 'a',
|
||||||
'min_w': 0.0, # soft limit min (mm), exposed as 4tn
|
'min_mm': 0.0, # soft limit min (mm), exposed as 4tn
|
||||||
'max_w': 100.0, # soft limit max (mm), exposed as 4tm
|
'max_mm': 100.0, # soft limit max (mm), exposed as 4tm
|
||||||
# Per-axis kinematic limits used to populate the planner's config.
|
# Per-axis kinematic limits used to populate the planner's config.
|
||||||
# Units match the bbctrl/onefinity per-motor convention so the
|
# Units match the bbctrl/onefinity per-motor convention so the
|
||||||
# values are directly comparable to motors 0-3:
|
# values are directly comparable to motors 0-3:
|
||||||
@@ -66,10 +66,34 @@ DEFAULTS = {
|
|||||||
'home_slow_sps': 250, # ≈ 10 mm/s
|
'home_slow_sps': 250, # ≈ 10 mm/s
|
||||||
'home_backoff_steps': 400, # ≈ 16 mm
|
'home_backoff_steps': 400, # ≈ 16 mm
|
||||||
'home_maxtravel_steps': 200000,
|
'home_maxtravel_steps': 200000,
|
||||||
|
# If HOME starts with the limit switch already tripped the ESP
|
||||||
|
# first moves this many steps away from the limit and then
|
||||||
|
# rechecks. If the switch is still active afterward, HOME hard-
|
||||||
|
# fails (refuses to set zero blindly when we may already be past
|
||||||
|
# the home position). Default ≈ 10 mm @ 25 steps/mm. Set to 0 to
|
||||||
|
# disable the preclear move (HOME then fails immediately if the
|
||||||
|
# switch reads active at start, matching the original behaviour).
|
||||||
|
'home_preclear_mm': 10.0,
|
||||||
'step_max_sps': 4000, # ≈ 160 mm/s normal-move cap
|
'step_max_sps': 4000, # ≈ 160 mm/s normal-move cap
|
||||||
'step_accel_sps2': 12000,
|
'step_accel_sps2': 12000,
|
||||||
'step_start_sps': 200,
|
'step_start_sps': 200,
|
||||||
'limit_low': True,
|
'limit_low': True,
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Z-A coupling interlock
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# The auxiliary A axis carries a tool that physically hangs below
|
||||||
|
# the Z-axis spindle nose. Beyond a certain Z descent the two
|
||||||
|
# collide unless A drops with Z. The constraint, in machine coords,
|
||||||
|
# is:
|
||||||
|
# A_machine - Z_machine <= K
|
||||||
|
# where K = (A_home_mm - z_home_mm) + couple_z_clearance_mm.
|
||||||
|
# When enabled this is enforced everywhere motion can be
|
||||||
|
# initiated (planner, MDI, jog, file load) and the AuxPreprocessor
|
||||||
|
# injects pre-position A moves before Z descends past the safe
|
||||||
|
# band.
|
||||||
|
'couple_z_enabled': True,
|
||||||
|
'couple_z_clearance_mm': 22.0, # Z drop allowed before A must follow
|
||||||
|
'z_home_mm': 0.0, # Z's machine position when homed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -120,23 +144,61 @@ class AuxAxis(object):
|
|||||||
def _config_path(self):
|
def _config_path(self):
|
||||||
return self.ctrl.get_path(filename='aux.json')
|
return self.ctrl.get_path(filename='aux.json')
|
||||||
|
|
||||||
|
# Legacy aux.json fields that have been renamed for clarity.
|
||||||
|
# Loaded values are migrated up on every load/save so existing
|
||||||
|
# installs keep working without operator intervention.
|
||||||
|
_LEGACY_FIELD_MAP = {
|
||||||
|
'min_w': 'min_mm',
|
||||||
|
'max_w': 'max_mm',
|
||||||
|
}
|
||||||
|
|
||||||
|
def _migrate_legacy_fields(self, cfg):
|
||||||
|
"""In-place rename of legacy keys in `cfg` (dict). Returns
|
||||||
|
True if anything was migrated, so callers can decide whether
|
||||||
|
to persist the upgraded form.
|
||||||
|
"""
|
||||||
|
migrated = False
|
||||||
|
for old, new in self._LEGACY_FIELD_MAP.items():
|
||||||
|
if old in cfg:
|
||||||
|
if new not in cfg:
|
||||||
|
cfg[new] = cfg[old]
|
||||||
|
del cfg[old]
|
||||||
|
migrated = True
|
||||||
|
return migrated
|
||||||
|
|
||||||
def _load_config(self):
|
def _load_config(self):
|
||||||
path = self._config_path()
|
path = self._config_path()
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
try:
|
try:
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
user = json.load(f)
|
user = json.load(f)
|
||||||
|
migrated = self._migrate_legacy_fields(user)
|
||||||
# Be permissive; ignore unknown keys.
|
# Be permissive; ignore unknown keys.
|
||||||
for k, v in user.items():
|
for k, v in user.items():
|
||||||
if k in self._cfg:
|
if k in self._cfg:
|
||||||
self._cfg[k] = v
|
self._cfg[k] = v
|
||||||
self.log.info('Loaded aux config from %s' % path)
|
self.log.info('Loaded aux config from %s' % path)
|
||||||
|
if migrated:
|
||||||
|
# Persist the upgraded form so future restarts
|
||||||
|
# see the new field names directly.
|
||||||
|
try:
|
||||||
|
self.save_config(self._cfg)
|
||||||
|
self.log.info(
|
||||||
|
'Migrated aux.json legacy fields '
|
||||||
|
'(min_w/max_w -> min_mm/max_mm)')
|
||||||
|
except Exception:
|
||||||
|
self.log.warning(
|
||||||
|
'Could not persist aux.json migration')
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.error('Failed to read aux.json: %s'
|
self.log.error('Failed to read aux.json: %s'
|
||||||
% traceback.format_exc())
|
% traceback.format_exc())
|
||||||
|
|
||||||
def save_config(self, cfg):
|
def save_config(self, cfg):
|
||||||
merged = dict(DEFAULTS)
|
merged = dict(DEFAULTS)
|
||||||
|
# Accept legacy keys from callers that may still send the
|
||||||
|
# old names (older UI bundles, hand-edited POSTs).
|
||||||
|
cfg = dict(cfg)
|
||||||
|
self._migrate_legacy_fields(cfg)
|
||||||
for k, v in cfg.items():
|
for k, v in cfg.items():
|
||||||
if k in DEFAULTS:
|
if k in DEFAULTS:
|
||||||
merged[k] = v
|
merged[k] = v
|
||||||
@@ -317,13 +379,13 @@ class AuxAxis(object):
|
|||||||
raise AuxAxisError('Aux axis not connected')
|
raise AuxAxisError('Aux axis not connected')
|
||||||
|
|
||||||
def _check_limits(self, target_mm):
|
def _check_limits(self, target_mm):
|
||||||
lo = float(self._cfg['min_w'])
|
lo = float(self._cfg['min_mm'])
|
||||||
hi = float(self._cfg['max_w'])
|
hi = float(self._cfg['max_mm'])
|
||||||
if hi <= lo:
|
if hi <= lo:
|
||||||
return # no limits
|
return # no limits
|
||||||
if target_mm < lo - 1e-6 or target_mm > hi + 1e-6:
|
if target_mm < lo - 1e-6 or target_mm > hi + 1e-6:
|
||||||
raise AuxAxisError(
|
raise AuxAxisError(
|
||||||
'W=%.3f out of soft limits [%.3f, %.3f]' % (target_mm, lo, hi))
|
'A=%.3f out of soft limits [%.3f, %.3f]' % (target_mm, lo, hi))
|
||||||
|
|
||||||
def _mm_to_steps(self, mm):
|
def _mm_to_steps(self, mm):
|
||||||
spm = float(self._cfg['steps_per_mm'])
|
spm = float(self._cfg['steps_per_mm'])
|
||||||
@@ -356,6 +418,64 @@ class AuxAxis(object):
|
|||||||
raise AuxAxisError('W move aborted by limit switch')
|
raise AuxAxisError('W move aborted by limit switch')
|
||||||
raise AuxAxisError('W move aborted: %s' % line)
|
raise AuxAxisError('W move aborted: %s' % line)
|
||||||
|
|
||||||
|
def _do_line(self, signed_steps, length_mm,
|
||||||
|
max_accel_mm_min2, max_jerk_mm_min3,
|
||||||
|
entry_vel_mm_min, exit_vel_mm_min,
|
||||||
|
times_min, ignore_limits=False, timeout=300.0):
|
||||||
|
"""Run a 7-segment jerk-limited S-curve on the ESP that mirrors
|
||||||
|
gplan/buildbotics' planner output exactly.
|
||||||
|
|
||||||
|
Parameters are in the same units the AVR/gplan use:
|
||||||
|
- length_mm: absolute travel in mm (>= 0)
|
||||||
|
- max_accel: mm/min^2
|
||||||
|
- max_jerk: mm/min^3
|
||||||
|
- entry/exit_vel: mm/min
|
||||||
|
- times_min: 7-tuple of section durations in minutes
|
||||||
|
|
||||||
|
ignore_limits sets safe=0 on the ESP - used for jog/move
|
||||||
|
endpoints that may run before homing.
|
||||||
|
|
||||||
|
Blocks until the ESP reports done or aborted. Updates the
|
||||||
|
position mirror and re-publishes state on every reply.
|
||||||
|
"""
|
||||||
|
if signed_steps == 0 or length_mm <= 0:
|
||||||
|
return
|
||||||
|
if not any(times_min):
|
||||||
|
raise AuxAxisError('LINE rejected: all section times are zero')
|
||||||
|
# Build the LINE command. Float formatting matches the AVR's
|
||||||
|
# printf precision (6 sig figs) - that's well above what the
|
||||||
|
# ESP needs given it integrates into a few thousand 4 ms
|
||||||
|
# segments per move.
|
||||||
|
parts = [
|
||||||
|
'LINE',
|
||||||
|
'steps=%d' % int(signed_steps),
|
||||||
|
'length=%.6f' % float(length_mm),
|
||||||
|
'max_accel=%.6f' % float(max_accel_mm_min2),
|
||||||
|
'max_jerk=%.6f' % float(max_jerk_mm_min3),
|
||||||
|
'entry_vel=%.6f' % float(entry_vel_mm_min),
|
||||||
|
'exit_vel=%.6f' % float(exit_vel_mm_min),
|
||||||
|
]
|
||||||
|
for i, t in enumerate(times_min):
|
||||||
|
if t and t > 0:
|
||||||
|
parts.append('t%d=%.9f' % (i, float(t)))
|
||||||
|
if ignore_limits:
|
||||||
|
parts.append('safe=0')
|
||||||
|
cmd = ' '.join(parts)
|
||||||
|
line = self._rpc(cmd, topic='line', timeout=timeout)
|
||||||
|
# line: "done pos=P emitted=N" or "aborted pos=P emitted=N reason=..."
|
||||||
|
if line.startswith('done'):
|
||||||
|
self._pos_steps = self._parse_kv_int(line, 'pos', self._pos_steps)
|
||||||
|
self._publish_state()
|
||||||
|
return
|
||||||
|
# aborted
|
||||||
|
self._pos_steps = self._parse_kv_int(line, 'pos', self._pos_steps)
|
||||||
|
self._publish_state()
|
||||||
|
reason = self._parse_kv_str(line, 'reason')
|
||||||
|
if reason == 'limit':
|
||||||
|
self._homed = False
|
||||||
|
raise AuxAxisError('W move aborted by limit switch')
|
||||||
|
raise AuxAxisError('W move aborted: %s' % line)
|
||||||
|
|
||||||
# ------------------------------------------------------------ serial I/O
|
# ------------------------------------------------------------ serial I/O
|
||||||
|
|
||||||
def _open(self):
|
def _open(self):
|
||||||
@@ -388,8 +508,14 @@ class AuxAxis(object):
|
|||||||
def _push_homecfg(self):
|
def _push_homecfg(self):
|
||||||
c = self._cfg
|
c = self._cfg
|
||||||
zero_steps = self._mm_to_steps(c['home_position_mm'])
|
zero_steps = self._mm_to_steps(c['home_position_mm'])
|
||||||
|
# preclear: how far (in steps) the ESP backs off if HOME is
|
||||||
|
# invoked while the limit switch is already tripped. Computed
|
||||||
|
# from home_preclear_mm so the operator configures it in mm.
|
||||||
|
spm = float(c.get('steps_per_mm', 1.0)) or 1.0
|
||||||
|
preclear_steps = int(round(abs(float(c['home_preclear_mm'])) * spm))
|
||||||
cmd = ('HOMECFG dir=%s fast=%d slow=%d backoff=%d maxtravel=%d '
|
cmd = ('HOMECFG dir=%s fast=%d slow=%d backoff=%d maxtravel=%d '
|
||||||
'zero=%d accel=%d step_max=%d step_start=%d limit_low=%d') % (
|
'zero=%d accel=%d step_max=%d step_start=%d limit_low=%d '
|
||||||
|
'preclear=%d') % (
|
||||||
c['home_dir'],
|
c['home_dir'],
|
||||||
int(c['home_fast_sps']),
|
int(c['home_fast_sps']),
|
||||||
int(c['home_slow_sps']),
|
int(c['home_slow_sps']),
|
||||||
@@ -400,6 +526,7 @@ class AuxAxis(object):
|
|||||||
int(c['step_max_sps']),
|
int(c['step_max_sps']),
|
||||||
int(c['step_start_sps']),
|
int(c['step_start_sps']),
|
||||||
1 if c['limit_low'] else 0,
|
1 if c['limit_low'] else 0,
|
||||||
|
preclear_steps,
|
||||||
)
|
)
|
||||||
self._rpc(cmd, topic='homecfg', timeout=3.0)
|
self._rpc(cmd, topic='homecfg', timeout=3.0)
|
||||||
|
|
||||||
@@ -409,11 +536,28 @@ class AuxAxis(object):
|
|||||||
self._pos_steps = int(r.strip())
|
self._pos_steps = int(r.strip())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# Force the host to start unhomed regardless of what the ESP
|
||||||
|
# remembers from a prior session. The ESP's homed flag survives
|
||||||
|
# bbctrl restarts (since the ESP itself wasn't power-cycled),
|
||||||
|
# but the host's planner offsets and DRO position get reset to
|
||||||
|
# zero on bbctrl boot. Trusting the ESP's homed flag would mean
|
||||||
|
# the user thinks A is homed at the wrong work-coord origin
|
||||||
|
# (offset_a=0 but ESP physically at home_position_mm). Sending
|
||||||
|
# UNHOME forces the user to re-home explicitly, which sets up
|
||||||
|
# the offset and gplan state correctly via the homing path in
|
||||||
|
# Mach.home.
|
||||||
try:
|
try:
|
||||||
r = self._rpc('HOMED?', topic='homed', timeout=2.0)
|
self._rpc('UNHOME', topic='ok', timeout=2.0)
|
||||||
self._homed = (r.strip() == '1')
|
self._homed = False
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
# Fall back to whatever HOMED? says - but treat any
|
||||||
|
# missing UNHOME support as "trust ESP's flag" so we
|
||||||
|
# don't break older firmware.
|
||||||
|
try:
|
||||||
|
r = self._rpc('HOMED?', topic='homed', timeout=2.0)
|
||||||
|
self._homed = (r.strip() == '1')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self._publish_state()
|
self._publish_state()
|
||||||
|
|
||||||
def _reader_loop(self):
|
def _reader_loop(self):
|
||||||
@@ -450,7 +594,7 @@ class AuxAxis(object):
|
|||||||
self._present = True
|
self._present = True
|
||||||
self._publish_state()
|
self._publish_state()
|
||||||
self.ctrl.state.add_message(
|
self.ctrl.state.add_message(
|
||||||
'W axis controller restarted - re-home before use')
|
'Auxiliary axis controller restarted - re-home before use')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Topic dispatch: "[topic] body..."
|
# Topic dispatch: "[topic] body..."
|
||||||
|
|||||||
@@ -59,15 +59,55 @@ _ATC_M_RE = re.compile(
|
|||||||
# A axis through gplan.)
|
# A axis through gplan.)
|
||||||
_W_TOKEN_RE = re.compile(r'(?<![A-Za-z_0-9])[Ww]\s*[-+]?\d*\.?\d+')
|
_W_TOKEN_RE = re.compile(r'(?<![A-Za-z_0-9])[Ww]\s*[-+]?\d*\.?\d+')
|
||||||
|
|
||||||
|
# Match a single axis word (letter + optional whitespace + signed decimal)
|
||||||
|
# for Z, A, X, Y. Used to extract modal targets while preserving the
|
||||||
|
# original line for emission. We deliberately ignore I/J/K/R (arc params)
|
||||||
|
# because they're not endpoints.
|
||||||
|
_AXIS_TOKEN_RES = {
|
||||||
|
'z': re.compile(r'(?<![A-Za-z_0-9])[Zz]\s*([-+]?\d*\.?\d+)'),
|
||||||
|
'a': re.compile(r'(?<![A-Za-z_0-9])[Aa]\s*([-+]?\d*\.?\d+)'),
|
||||||
|
'x': re.compile(r'(?<![A-Za-z_0-9])[Xx]\s*([-+]?\d*\.?\d+)'),
|
||||||
|
'y': re.compile(r'(?<![A-Za-z_0-9])[Yy]\s*([-+]?\d*\.?\d+)'),
|
||||||
|
}
|
||||||
|
_G_CODE_RE = re.compile(r'(?<![A-Za-z_0-9])[Gg]\s*0*(\d+(?:\.\d+)?)')
|
||||||
|
|
||||||
|
|
||||||
class AuxPreprocessorError(Exception):
|
class AuxPreprocessorError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AuxPreprocessor(object):
|
class AuxPreprocessor(object):
|
||||||
def __init__(self, log=None):
|
def __init__(self, log=None, coupling=None):
|
||||||
|
"""`coupling`, when supplied, enables Z-A coupling injection.
|
||||||
|
Expected shape:
|
||||||
|
{
|
||||||
|
'enabled': bool,
|
||||||
|
'clearance_mm': float, # max (A_wc - Z_wc)
|
||||||
|
'a_initial_wc': float, # A's work-coord position at
|
||||||
|
# file start (typically 0 if
|
||||||
|
# operator zeroed at home)
|
||||||
|
'z_initial_wc': float, # Z's work-coord position at
|
||||||
|
# file start (typically 0)
|
||||||
|
}
|
||||||
|
Pass None to disable injection (preprocessor still rewrites
|
||||||
|
ATC M-codes)."""
|
||||||
self.log = log
|
self.log = log
|
||||||
self._w_warned = False
|
self._w_warned = False
|
||||||
|
self._coupling = coupling if (coupling and
|
||||||
|
coupling.get('enabled')) else None
|
||||||
|
# Modal state used while scanning the file.
|
||||||
|
if self._coupling is not None:
|
||||||
|
self._a_wc = float(coupling.get('a_initial_wc', 0.0))
|
||||||
|
self._z_wc = float(coupling.get('z_initial_wc', 0.0))
|
||||||
|
self._K = float(coupling.get('clearance_mm', 0.0))
|
||||||
|
else:
|
||||||
|
self._a_wc = 0.0
|
||||||
|
self._z_wc = 0.0
|
||||||
|
self._K = 0.0
|
||||||
|
self._g91_warned = False
|
||||||
|
# Distance mode: True for absolute (G90), False for incremental
|
||||||
|
# (G91). Per RS274 the modal default at start is G90.
|
||||||
|
self._g90 = True
|
||||||
|
|
||||||
def _info(self, msg):
|
def _info(self, msg):
|
||||||
if self.log: self.log.info(msg)
|
if self.log: self.log.info(msg)
|
||||||
@@ -78,9 +118,12 @@ class AuxPreprocessor(object):
|
|||||||
# ------------------------------------------------------------------ scan
|
# ------------------------------------------------------------------ scan
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def file_uses_aux(path):
|
def file_uses_aux(path, coupling=None):
|
||||||
"""Quick check: does this file contain anything the preprocessor
|
"""Quick check: does this file contain anything the preprocessor
|
||||||
would rewrite (currently: just ATC M-codes)?"""
|
would rewrite? Returns True for ATC M-codes always, and for
|
||||||
|
any Z/A move if coupling is enabled (we have to scan to know
|
||||||
|
whether injection is needed, so any motion file qualifies)."""
|
||||||
|
couple_active = bool(coupling and coupling.get('enabled'))
|
||||||
try:
|
try:
|
||||||
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
with open(path, 'r', encoding='utf-8', errors='replace') as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
@@ -88,6 +131,10 @@ class AuxPreprocessor(object):
|
|||||||
code = code.split(';', 1)[0]
|
code = code.split(';', 1)[0]
|
||||||
if _ATC_M_RE.search(code):
|
if _ATC_M_RE.search(code):
|
||||||
return True
|
return True
|
||||||
|
if couple_active:
|
||||||
|
if _AXIS_TOKEN_RES['z'].search(code) or \
|
||||||
|
_AXIS_TOKEN_RES['a'].search(code):
|
||||||
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
@@ -95,6 +142,170 @@ class AuxPreprocessor(object):
|
|||||||
# Backwards-compat alias.
|
# Backwards-compat alias.
|
||||||
file_uses_w = file_uses_aux
|
file_uses_w = file_uses_aux
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ Z-A coupling
|
||||||
|
#
|
||||||
|
# Track modal Z and A targets across the file. Whenever a line
|
||||||
|
# would put A above Z by more than `clearance_mm` (i.e. A_wc -
|
||||||
|
# Z_wc > K), we inject `G0 A<safe>` immediately before it so A is
|
||||||
|
# already at the safe position when Z descends. The injected move
|
||||||
|
# uses G0 (rapid) so it's quick.
|
||||||
|
#
|
||||||
|
# Endpoint-only check: gplan plans line endpoints. As long as
|
||||||
|
# (target_A_wc - target_Z_wc) <= K, the trajectory stays safe
|
||||||
|
# because Z's *minimum* during a single line is its endpoint (Z
|
||||||
|
# moves monotonically along a single line block in absolute
|
||||||
|
# mode) and A is held at the pre-positioned value during the move.
|
||||||
|
|
||||||
|
def _extract_g_codes(self, code):
|
||||||
|
"""Return the set of G-codes referenced on `code`. Numeric
|
||||||
|
only, e.g. {0, 1, 90, 17}. Used to track modal state."""
|
||||||
|
out = set()
|
||||||
|
for m in _G_CODE_RE.finditer(code):
|
||||||
|
try:
|
||||||
|
out.add(int(float(m.group(1))))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _extract_axis(self, axis, code):
|
||||||
|
"""Return the last value of `axis` token on `code`, or None."""
|
||||||
|
rx = _AXIS_TOKEN_RES.get(axis)
|
||||||
|
if rx is None:
|
||||||
|
return None
|
||||||
|
last = None
|
||||||
|
for m in rx.finditer(code):
|
||||||
|
try:
|
||||||
|
last = float(m.group(1))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return last
|
||||||
|
|
||||||
|
def _maybe_inject_a_down(self, code, fout):
|
||||||
|
"""Inspect `code` (with comments stripped) for an upcoming Z
|
||||||
|
descent; emit a `G0 A<safe>` line on `fout` if needed and
|
||||||
|
update self._a_wc accordingly. Returns True if anything was
|
||||||
|
injected.
|
||||||
|
|
||||||
|
On a violation that cannot be fixed by lowering A (e.g. the
|
||||||
|
operator wrote `G0 A0` while Z is too deep), raise
|
||||||
|
AuxPreprocessorError so the file load surfaces the problem -
|
||||||
|
per the rule we agreed: error, don't silently insert a Z-up.
|
||||||
|
"""
|
||||||
|
if self._coupling is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Distance mode tracking.
|
||||||
|
gs = self._extract_g_codes(code)
|
||||||
|
if 90 in gs: self._g90 = True
|
||||||
|
if 91 in gs:
|
||||||
|
if self._g90 and not self._g91_warned:
|
||||||
|
self._warn(
|
||||||
|
'AuxPreprocessor: G91 (incremental mode) detected; '
|
||||||
|
'Z-A coupling injection is disabled for the rest of '
|
||||||
|
'the file. The runtime check still applies.')
|
||||||
|
self._g91_warned = True
|
||||||
|
self._g90 = False
|
||||||
|
|
||||||
|
# G92 sets coordinate offsets. The new modal value of an
|
||||||
|
# axis is whatever value follows on the same word (e.g.
|
||||||
|
# G92 A0 sets A_wc = 0). Apply that and skip injection.
|
||||||
|
if 92 in gs:
|
||||||
|
new_a = self._extract_axis('a', code)
|
||||||
|
new_z = self._extract_axis('z', code)
|
||||||
|
if new_a is not None: self._a_wc = new_a
|
||||||
|
if new_z is not None: self._z_wc = new_z
|
||||||
|
return False
|
||||||
|
|
||||||
|
# In incremental mode we can still track approximately, but
|
||||||
|
# the user has been warned; skip injection.
|
||||||
|
if not self._g90:
|
||||||
|
return False
|
||||||
|
|
||||||
|
new_z_target = self._extract_axis('z', code)
|
||||||
|
new_a_target = self._extract_axis('a', code)
|
||||||
|
if new_z_target is None and new_a_target is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Modal values after the line executes.
|
||||||
|
a_after = new_a_target if new_a_target is not None else self._a_wc
|
||||||
|
z_after = new_z_target if new_z_target is not None else self._z_wc
|
||||||
|
|
||||||
|
eps = 1e-4
|
||||||
|
if a_after - z_after <= self._K + eps:
|
||||||
|
# Move is safe as authored. Update modal state.
|
||||||
|
self._a_wc = a_after
|
||||||
|
self._z_wc = z_after
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Violation. Two cases:
|
||||||
|
#
|
||||||
|
# (a) The line lowers Z (z_after < self._z_wc) and A is
|
||||||
|
# held or moved upward, so A needs to drop to keep up.
|
||||||
|
# We can fix this by pre-positioning A at z_after + K
|
||||||
|
# BEFORE the line - at which point gplan's plan for the
|
||||||
|
# line is safe at every point along it.
|
||||||
|
#
|
||||||
|
# (b) The line raises A above the safe band while Z is
|
||||||
|
# held (z_after >= self._z_wc) - i.e. the operator
|
||||||
|
# wrote `G0 A0` while Z is parked deep. Auto-injecting
|
||||||
|
# a Z-up here is unsafe (Z could swing into a fixture
|
||||||
|
# or the part) so we error out and let the operator
|
||||||
|
# author the lift.
|
||||||
|
|
||||||
|
safe_a = z_after + self._K
|
||||||
|
|
||||||
|
# If the line itself targets an A above the safe band, the
|
||||||
|
# endpoint violates the rule no matter what we pre-position.
|
||||||
|
# Refuse rather than emit something that runs the gantry into
|
||||||
|
# the tool.
|
||||||
|
if new_a_target is not None and new_a_target > safe_a + eps:
|
||||||
|
raise AuxPreprocessorError(
|
||||||
|
'Z-A coupling violation: line targets A=%.3f at '
|
||||||
|
'Z=%.3f, but max A allowed is %.3f (clearance %.3f). '
|
||||||
|
'Lower the A target or add a Z-up move first.' % (
|
||||||
|
new_a_target, z_after, safe_a, self._K))
|
||||||
|
|
||||||
|
# If the line raises A above the current safe band but Z
|
||||||
|
# isn't dropping with it (no Z target on the line, or Z stays
|
||||||
|
# put), the violation is the operator's A-up, not a Z-down.
|
||||||
|
# Refuse rather than insert a Z-up (which could swing through
|
||||||
|
# a fixture or part).
|
||||||
|
if (new_a_target is not None and
|
||||||
|
new_a_target > self._a_wc + eps and
|
||||||
|
new_z_target is None):
|
||||||
|
raise AuxPreprocessorError(
|
||||||
|
'Z-A coupling violation at line raising A to %.3f '
|
||||||
|
'while Z is at %.3f (max A allowed is %.3f given '
|
||||||
|
'clearance %.3f). Add a Z-up move first.' % (
|
||||||
|
new_a_target, z_after, safe_a, self._K))
|
||||||
|
|
||||||
|
# Case (a): pre-position A.
|
||||||
|
# Don't move A *up* as part of pre-position - if the safe
|
||||||
|
# value is above where A already is, we'd lift A into a
|
||||||
|
# potential collision elsewhere. In practice safe_a < a_wc
|
||||||
|
# whenever we get here (otherwise no violation), but assert
|
||||||
|
# to be sure.
|
||||||
|
if safe_a > self._a_wc + eps:
|
||||||
|
raise AuxPreprocessorError(
|
||||||
|
'Z-A coupling: cannot fix line by lowering A '
|
||||||
|
'(safe A = %.3f > current A = %.3f).' % (
|
||||||
|
safe_a, self._a_wc))
|
||||||
|
fout.write('(injected by AuxPreprocessor: Z-A coupling)\n')
|
||||||
|
fout.write('G0 A%.4f\n' % safe_a)
|
||||||
|
self._a_wc = safe_a
|
||||||
|
# Don't update z_wc yet - the original line will do that
|
||||||
|
# when it runs. But our modal copy must reflect the post-line
|
||||||
|
# value so subsequent injections compute correctly.
|
||||||
|
self._z_wc = z_after
|
||||||
|
# If the original line also moved A, our pre-positioning
|
||||||
|
# supersedes it (we overwrite a_wc above with safe_a then
|
||||||
|
# the original line's A target may push it back up). Update
|
||||||
|
# a_wc to the line's authored A value so further checks see
|
||||||
|
# the post-line state.
|
||||||
|
if new_a_target is not None:
|
||||||
|
self._a_wc = new_a_target
|
||||||
|
return True
|
||||||
|
|
||||||
# ------------------------------------------------------------------ run
|
# ------------------------------------------------------------------ run
|
||||||
|
|
||||||
def process(self, src_path, dst_path):
|
def process(self, src_path, dst_path):
|
||||||
@@ -124,6 +335,10 @@ class AuxPreprocessor(object):
|
|||||||
'subsequent W tokens in this file)')
|
'subsequent W tokens in this file)')
|
||||||
self._w_warned = True
|
self._w_warned = True
|
||||||
|
|
||||||
|
# Z-A coupling injection BEFORE the line is emitted.
|
||||||
|
if self._maybe_inject_a_down(code, fout):
|
||||||
|
rewrote_any = True
|
||||||
|
|
||||||
# ATC M-codes (M100-M103). Each ATC M-code on the line
|
# ATC M-codes (M100-M103). Each ATC M-code on the line
|
||||||
# is replaced with its (MSG,HOOK:<event>:) line and
|
# is replaced with its (MSG,HOOK:<event>:) line and
|
||||||
# stripped from the residual.
|
# stripped from the residual.
|
||||||
@@ -156,15 +371,17 @@ class AuxPreprocessor(object):
|
|||||||
return rewrote_any
|
return rewrote_any
|
||||||
|
|
||||||
|
|
||||||
def preprocess_file(src_path, log=None, **_unused):
|
def preprocess_file(src_path, log=None, coupling=None, **_unused):
|
||||||
"""Convenience: rewrite src_path in place if it contains ATC
|
"""Convenience: rewrite src_path in place if it contains ATC
|
||||||
M-codes. Returns True if the file was rewritten.
|
M-codes or needs Z-A coupling injection. Returns True if the
|
||||||
|
file was rewritten.
|
||||||
|
|
||||||
|
`coupling` is an optional dict (see AuxPreprocessor.__init__).
|
||||||
Extra keyword args are accepted for backwards compat (the old
|
Extra keyword args are accepted for backwards compat (the old
|
||||||
w_first arg is no longer used)."""
|
w_first arg is no longer used)."""
|
||||||
if not AuxPreprocessor.file_uses_aux(src_path):
|
if not AuxPreprocessor.file_uses_aux(src_path, coupling=coupling):
|
||||||
return False
|
return False
|
||||||
pre = AuxPreprocessor(log=log)
|
pre = AuxPreprocessor(log=log, coupling=coupling)
|
||||||
fd, tmp = tempfile.mkstemp(prefix='auxpre_', suffix='.nc',
|
fd, tmp = tempfile.mkstemp(prefix='auxpre_', suffix='.nc',
|
||||||
dir=os.path.dirname(src_path) or None)
|
dir=os.path.dirname(src_path) or None)
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
|||||||
@@ -216,6 +216,32 @@ class Config(object):
|
|||||||
defaults = json.load(f)
|
defaults = json.load(f)
|
||||||
config['selected-tool-settings'] = defaults['selected-tool-settings'];
|
config['selected-tool-settings'] = defaults['selected-tool-settings'];
|
||||||
|
|
||||||
|
# Auxiliary axis nomenclature: rename W -> A in macro names and
|
||||||
|
# filenames. The auxcnc-driven stepper has been integrated into
|
||||||
|
# gplan as A since the option-b migration; old configs may
|
||||||
|
# still carry W Down/W Up macro entries pointing at
|
||||||
|
# w_down.nc/w_up.nc which were renamed on disk to a_down.nc /
|
||||||
|
# a_up.nc. Migrate idempotently on every load so a stale
|
||||||
|
# in-memory copy can never reintroduce the old names.
|
||||||
|
macros = config.get('macros') if isinstance(config, dict) else None
|
||||||
|
if isinstance(macros, list):
|
||||||
|
renames = {
|
||||||
|
'w_down.nc': 'a_down.nc',
|
||||||
|
'w_up.nc': 'a_up.nc',
|
||||||
|
}
|
||||||
|
display_renames = {
|
||||||
|
'W Down': 'A Down',
|
||||||
|
'W Up': 'A Up',
|
||||||
|
}
|
||||||
|
for m in macros:
|
||||||
|
if not isinstance(m, dict): continue
|
||||||
|
fn = m.get('file_name')
|
||||||
|
if isinstance(fn, str) and fn in renames:
|
||||||
|
m['file_name'] = renames[fn]
|
||||||
|
nm = m.get('name')
|
||||||
|
if isinstance(nm, str) and nm in display_renames:
|
||||||
|
m['name'] = display_renames[nm]
|
||||||
|
|
||||||
config['version'] = self.version.split('b')[0]
|
config['version'] = self.version.split('b')[0]
|
||||||
config['full_version'] = self.version
|
config['full_version'] = self.version
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,192 @@ class ExternalAxis(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
# ------------------------------------------------------- soft limits
|
||||||
|
|
||||||
|
def _soft_limits(self):
|
||||||
|
"""Return (min_mm, max_mm) in machine coords, or (None, None)
|
||||||
|
if soft limits are disabled (max <= min)."""
|
||||||
|
try:
|
||||||
|
lo = float(self.aux._cfg.get('min_mm', 0.0))
|
||||||
|
hi = float(self.aux._cfg.get('max_mm', 0.0))
|
||||||
|
except Exception:
|
||||||
|
return (None, None)
|
||||||
|
if hi <= lo:
|
||||||
|
return (None, None)
|
||||||
|
return (lo, hi)
|
||||||
|
|
||||||
|
def _check_soft_limit(self, target_abs_mm):
|
||||||
|
"""Raise ExternalAxisError if target_abs_mm is outside the
|
||||||
|
configured soft limits. Skips the check when the axis isn't
|
||||||
|
homed (matching the standard bbctrl convention that soft
|
||||||
|
limits are gated by homing state) - that lets the user jog
|
||||||
|
away from a stuck position before homing without false
|
||||||
|
rejections.
|
||||||
|
|
||||||
|
Called by both planner-driven motion (enqueue_target_mm) and
|
||||||
|
UI motion (execute_to_mm), so this is the single source of
|
||||||
|
truth regardless of which path triggered the move."""
|
||||||
|
# Honour the homing gate.
|
||||||
|
try:
|
||||||
|
homed = bool(self.aux._homed)
|
||||||
|
except Exception:
|
||||||
|
homed = False
|
||||||
|
if not homed:
|
||||||
|
return
|
||||||
|
lo, hi = self._soft_limits()
|
||||||
|
if lo is None:
|
||||||
|
return
|
||||||
|
# Use a tiny epsilon so floating-point round-trip targets
|
||||||
|
# right at the boundary aren't rejected.
|
||||||
|
eps = 1e-4
|
||||||
|
target = float(target_abs_mm)
|
||||||
|
if target < lo - eps or target > hi + eps:
|
||||||
|
raise ExternalAxisError(
|
||||||
|
'%s axis target %.4f mm is outside soft limits '
|
||||||
|
'[%.3f, %.3f] mm' % (
|
||||||
|
self.axis_letter.upper(), target, lo, hi))
|
||||||
|
|
||||||
|
# ----------------------------------------------- Z-A coupling
|
||||||
|
#
|
||||||
|
# The auxiliary tool hangs below the Z spindle. Beyond a small
|
||||||
|
# Z descent the two collide unless A drops with Z. The
|
||||||
|
# constraint, in machine coords, is
|
||||||
|
#
|
||||||
|
# A_machine - Z_machine <= K
|
||||||
|
# K = (A_home_mm - z_home_mm) + couple_z_clearance_mm
|
||||||
|
#
|
||||||
|
# Enforced before any motion (planner blocks, MDI, jogs). The
|
||||||
|
# AuxPreprocessor injects pre-position A moves into uploaded
|
||||||
|
# files so well-formed gcode runs without having to think about
|
||||||
|
# this. Disabled when couple_z_enabled is false.
|
||||||
|
|
||||||
|
@property
|
||||||
|
def couple_z_enabled(self):
|
||||||
|
try:
|
||||||
|
return bool(self.aux._cfg.get('couple_z_enabled', False))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def couple_K(self):
|
||||||
|
"""Limit constant K (machine-coord units): the maximum value
|
||||||
|
of (A_machine - Z_machine) before the tool collides. Returns
|
||||||
|
None if the rule isn't applicable (coupling disabled or
|
||||||
|
config missing)."""
|
||||||
|
try:
|
||||||
|
cfg = self.aux._cfg
|
||||||
|
clearance = float(cfg.get('couple_z_clearance_mm', 0.0))
|
||||||
|
a_home = float(cfg.get('home_position_mm', 0.0))
|
||||||
|
z_home = float(cfg.get('z_home_mm', 0.0))
|
||||||
|
return (a_home - z_home) + clearance
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def couple_clearance_mm(self):
|
||||||
|
"""Raw clearance from config: how far Z may travel below its
|
||||||
|
home before A has to start dropping with it. Used by the
|
||||||
|
AuxPreprocessor to inject pre-position A moves into uploaded
|
||||||
|
gcode."""
|
||||||
|
try:
|
||||||
|
return float(self.aux._cfg.get('couple_z_clearance_mm', 0.0))
|
||||||
|
except Exception:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _z_machine_now(self):
|
||||||
|
"""Read Z's current machine position from State, or None if
|
||||||
|
Z isn't homed/reported yet. The AVR reports absolute machine
|
||||||
|
positions in <axis>p; the work-coord display is computed by
|
||||||
|
the UI as zp - offset_z, but here we want machine directly."""
|
||||||
|
try:
|
||||||
|
st = self.ctrl.state
|
||||||
|
zp = st.get('zp', None)
|
||||||
|
if zp is None:
|
||||||
|
return None
|
||||||
|
return float(zp)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _a_machine_now(self):
|
||||||
|
"""A's current machine position. ExternalAxis tracks this
|
||||||
|
directly in self._pos_mm (mm in machine coords - we don't
|
||||||
|
apply G92 to A internally; offset_a is informational)."""
|
||||||
|
try:
|
||||||
|
if self._pos_mm is not None:
|
||||||
|
return float(self._pos_mm)
|
||||||
|
# Fall back to whatever the ESP last reported.
|
||||||
|
return float(self.aux.position_mm)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def coupling_for_preprocessor(self):
|
||||||
|
"""Return the dict the AuxPreprocessor wants for in-file
|
||||||
|
injection, or None when coupling is off. We assume the
|
||||||
|
operator authors gcode in a frame where the at-home position
|
||||||
|
is A_wc=0, Z_wc=0 - which matches our home-zeroed setup.
|
||||||
|
Files that use a different convention will fall through to
|
||||||
|
the runtime check."""
|
||||||
|
if not self.couple_z_enabled:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
'enabled': True,
|
||||||
|
'clearance_mm': self.couple_clearance_mm,
|
||||||
|
'a_initial_wc': 0.0,
|
||||||
|
'z_initial_wc': 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
def check_coupling(self, target_a_machine=None, target_z_machine=None):
|
||||||
|
"""Validate that a proposed motion respects the Z-A coupling.
|
||||||
|
|
||||||
|
Each argument is a target *machine* mm position; pass None to
|
||||||
|
keep the current value of that axis.
|
||||||
|
|
||||||
|
Improvement-aware: a move is rejected only when it *worsens*
|
||||||
|
an already-violating state (or moves a healthy state into
|
||||||
|
violation). Pure XY jogs that touch neither Z nor A are not
|
||||||
|
passed through here; jogs that hold Z or A at their current
|
||||||
|
value (gplan emits the unchanged value in `target`) pass
|
||||||
|
because (a-z) doesn't change. Z-up moves while in violation
|
||||||
|
also pass because they reduce (a-z) toward the bound.
|
||||||
|
|
||||||
|
Raises ExternalAxisError on violation. Skipped when coupling
|
||||||
|
is disabled, the aux axis isn't homed, or current positions
|
||||||
|
aren't yet known.
|
||||||
|
"""
|
||||||
|
if not self.couple_z_enabled:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
homed = bool(self.aux._homed)
|
||||||
|
except Exception:
|
||||||
|
homed = False
|
||||||
|
if not homed:
|
||||||
|
return
|
||||||
|
K = self.couple_K
|
||||||
|
if K is None:
|
||||||
|
return
|
||||||
|
a_now = self._a_machine_now()
|
||||||
|
z_now = self._z_machine_now()
|
||||||
|
if a_now is None or z_now is None:
|
||||||
|
return
|
||||||
|
a_after = (float(target_a_machine)
|
||||||
|
if target_a_machine is not None else a_now)
|
||||||
|
z_after = (float(target_z_machine)
|
||||||
|
if target_z_machine is not None else z_now)
|
||||||
|
eps = 1e-4
|
||||||
|
gap_after = a_after - z_after
|
||||||
|
gap_before = a_now - z_now
|
||||||
|
# Only refuse when (a) the resulting state would violate the
|
||||||
|
# constraint AND (b) the move makes things at least as bad
|
||||||
|
# as the current state. This lets the operator escape an
|
||||||
|
# already-violating state by moving in the right direction
|
||||||
|
# (Z up, A down).
|
||||||
|
if gap_after > K + eps and gap_after > gap_before - eps:
|
||||||
|
raise ExternalAxisError(
|
||||||
|
'Z-A coupling violation: A=%.3f mm and Z=%.3f mm '
|
||||||
|
'(machine) would put A above Z by %.3f mm; max '
|
||||||
|
'allowed is %.3f mm. Drop A or raise Z first.' % (
|
||||||
|
a_after, z_after, gap_after, K))
|
||||||
|
|
||||||
# ----------------------------------------------------------- conversion
|
# ----------------------------------------------------------- conversion
|
||||||
|
|
||||||
def mm_to_steps_delta(self, delta_mm):
|
def mm_to_steps_delta(self, delta_mm):
|
||||||
@@ -185,8 +371,8 @@ class ExternalAxis(object):
|
|||||||
|
|
||||||
# Soft limits in machine units (mm). State.get_soft_limit_vector
|
# Soft limits in machine units (mm). State.get_soft_limit_vector
|
||||||
# returns these directly, no scaling.
|
# returns these directly, no scaling.
|
||||||
st.set(i + 'tn', float(cfg.get('min_w', 0.0)))
|
st.set(i + 'tn', float(cfg.get('min_mm', 0.0)))
|
||||||
st.set(i + 'tm', float(cfg.get('max_w', 0.0)))
|
st.set(i + 'tm', float(cfg.get('max_mm', 0.0)))
|
||||||
|
|
||||||
# home_position / home_travel are exposed as callbacks for
|
# home_position / home_travel are exposed as callbacks for
|
||||||
# motors 0..3 (see State.__init__). Register the same lazy
|
# motors 0..3 (see State.__init__). Register the same lazy
|
||||||
@@ -197,7 +383,7 @@ class ExternalAxis(object):
|
|||||||
i + 'home_position', lambda name: self.home_position_mm)
|
i + 'home_position', lambda name: self.home_position_mm)
|
||||||
st.set_callback(
|
st.set_callback(
|
||||||
i + 'home_travel',
|
i + 'home_travel',
|
||||||
lambda name: float(self.aux._cfg.get('max_w', 0.0))
|
lambda name: float(self.aux._cfg.get('max_mm', 0.0))
|
||||||
- self.home_position_mm)
|
- self.home_position_mm)
|
||||||
|
|
||||||
# Misc fields that other code paths might query. Defaults
|
# Misc fields that other code paths might query. Defaults
|
||||||
@@ -265,12 +451,23 @@ class ExternalAxis(object):
|
|||||||
"""Synchronously run an external move. Blocks until the ESP
|
"""Synchronously run an external move. Blocks until the ESP
|
||||||
reports done. Used by the legacy /api/aux/move and /api/aux/jog
|
reports done. Used by the legacy /api/aux/move and /api/aux/jog
|
||||||
endpoints which may want to wait. Most planner-driven motion
|
endpoints which may want to wait. Most planner-driven motion
|
||||||
goes through enqueue_target_mm instead, which is non-blocking."""
|
goes through enqueue_target_mm instead, which is non-blocking.
|
||||||
|
|
||||||
|
Soft limits are enforced here (not just in gplan) because the
|
||||||
|
UI jog/move endpoints don't go through the planner.
|
||||||
|
|
||||||
|
Updates state.<axis>p immediately on completion. For the
|
||||||
|
planner-driven path that goes through enqueue_target_mm, the
|
||||||
|
AVR's own ap reports drive state.<axis>p instead."""
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
raise ExternalAxisError(
|
raise ExternalAxisError(
|
||||||
'External axis %r not available (aux disabled or '
|
'External axis %r not available (aux disabled or '
|
||||||
'not connected)' % self.axis_letter)
|
'not connected)' % self.axis_letter)
|
||||||
|
|
||||||
|
self._check_soft_limit(ext_mm)
|
||||||
|
# Coupling: A is in machine coords directly (we don't apply
|
||||||
|
# a G92 offset to A), so target_a_machine == ext_mm.
|
||||||
|
self.check_coupling(target_a_machine=ext_mm)
|
||||||
steps, abs_mm = self._compute_move(ext_mm)
|
steps, abs_mm = self._compute_move(ext_mm)
|
||||||
if steps == 0:
|
if steps == 0:
|
||||||
self._pos_mm = abs_mm
|
self._pos_mm = abs_mm
|
||||||
@@ -286,31 +483,69 @@ class ExternalAxis(object):
|
|||||||
self._busy.clear()
|
self._busy.clear()
|
||||||
|
|
||||||
def enqueue_target_mm(self, ext_mm):
|
def enqueue_target_mm(self, ext_mm):
|
||||||
"""Non-blocking variant: post a target to the worker queue
|
"""Legacy non-blocking variant: post a fixed-rate STEPS move
|
||||||
and update the host's notion of the axis position immediately
|
to the worker queue. No longer used by Planner.__encode (which
|
||||||
so subsequent line splits compute correct deltas.
|
uses enqueue_line for full S-curve mirroring), but kept for
|
||||||
|
UI jog endpoints that don't have planner timing data.
|
||||||
|
|
||||||
The Planner.__encode hook calls this so the AVR comm thread
|
Soft limits are enforced here (defense in depth on top of
|
||||||
is never blocked by serial RPCs to the ESP. v1 accepts that
|
gplan)."""
|
||||||
XYZ on the AVR and A on the ESP run concurrently when they
|
|
||||||
appear on the same gcode line; the planner's S-curve math is
|
|
||||||
applied to both, so velocities and accelerations are bounded
|
|
||||||
by whichever axis is most constrained."""
|
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
raise ExternalAxisError(
|
raise ExternalAxisError(
|
||||||
'External axis %r not available' % self.axis_letter)
|
'External axis %r not available' % self.axis_letter)
|
||||||
|
self._check_soft_limit(ext_mm)
|
||||||
|
self.check_coupling(target_a_machine=ext_mm)
|
||||||
steps, abs_mm = self._compute_move(ext_mm)
|
steps, abs_mm = self._compute_move(ext_mm)
|
||||||
# Update host position immediately so the next line block
|
# Internal mirror only - drives subsequent delta computation.
|
||||||
# sees the new absolute target as the starting point.
|
# state.<axis>p is left to the AVR's status reports.
|
||||||
self._pos_mm = abs_mm
|
self._pos_mm = abs_mm
|
||||||
self.ctrl.state.set(self.axis_letter + 'p', self._pos_mm)
|
|
||||||
if steps == 0:
|
if steps == 0:
|
||||||
return
|
return
|
||||||
# Enqueue. The worker fires the RPC; if it fails it logs
|
|
||||||
# and we keep going - aborting motion is the user's job
|
|
||||||
# via the planner stop / e-stop.
|
|
||||||
self._work_q.put(('move', steps))
|
self._work_q.put(('move', steps))
|
||||||
|
|
||||||
|
def enqueue_line(self, ext_mm, max_accel_mm_min2, max_jerk_mm_min3,
|
||||||
|
entry_vel_mm_min, exit_vel_mm_min, times_ms):
|
||||||
|
"""Post a full S-curve LINE block to the ESP worker. Mirrors
|
||||||
|
gplan's planned trajectory exactly (same 7-segment math, same
|
||||||
|
unit system) so the ESP's move duration matches what the AVR
|
||||||
|
would have produced for an A motor.
|
||||||
|
|
||||||
|
Called by Planner.__encode for every line block that touches
|
||||||
|
the external axis.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
ext_mm: absolute target in mm (gplan target['a'])
|
||||||
|
max_accel_mm_min2:from block['max-accel']
|
||||||
|
max_jerk_mm_min3: from block['max-jerk']
|
||||||
|
entry_vel_mm_min: from block['entry-vel'] (typically 0 for
|
||||||
|
the first block, exit_vel of the prior
|
||||||
|
block otherwise)
|
||||||
|
exit_vel_mm_min: from block['exit-vel']
|
||||||
|
times_ms: 7-tuple of section durations in ms
|
||||||
|
(block['times'] - the same units gplan uses)
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
raise ExternalAxisError(
|
||||||
|
'External axis %r not available' % self.axis_letter)
|
||||||
|
self._check_soft_limit(ext_mm)
|
||||||
|
self.check_coupling(target_a_machine=ext_mm)
|
||||||
|
steps, abs_mm = self._compute_move(ext_mm)
|
||||||
|
delta_mm = abs(abs_mm - (self._pos_mm if self._pos_mm is not None
|
||||||
|
else 0.0))
|
||||||
|
# Update internal mirror; AVR drives state.<axis>p.
|
||||||
|
self._pos_mm = abs_mm
|
||||||
|
if steps == 0 or delta_mm <= 0:
|
||||||
|
return
|
||||||
|
# ms -> minutes (the unit gplan/AVR/ESP use internally for
|
||||||
|
# SCurve math).
|
||||||
|
times_min = tuple((t / 60000.0) if t else 0.0 for t in times_ms)
|
||||||
|
self._work_q.put(('line', steps, delta_mm,
|
||||||
|
float(max_accel_mm_min2),
|
||||||
|
float(max_jerk_mm_min3),
|
||||||
|
float(entry_vel_mm_min),
|
||||||
|
float(exit_vel_mm_min),
|
||||||
|
times_min))
|
||||||
|
|
||||||
def _compute_move(self, ext_mm):
|
def _compute_move(self, ext_mm):
|
||||||
"""Return (signed_steps, absolute_mm) for a target in mm.
|
"""Return (signed_steps, absolute_mm) for a target in mm.
|
||||||
Caches first-time position from the ESP."""
|
Caches first-time position from the ESP."""
|
||||||
@@ -338,6 +573,15 @@ class ExternalAxis(object):
|
|||||||
if kind == 'move':
|
if kind == 'move':
|
||||||
steps = op[1]
|
steps = op[1]
|
||||||
self.aux._do_steps(steps, ignore_limits=True)
|
self.aux._do_steps(steps, ignore_limits=True)
|
||||||
|
elif kind == 'line':
|
||||||
|
(_, steps, length_mm,
|
||||||
|
max_accel, max_jerk,
|
||||||
|
entry_vel, exit_vel,
|
||||||
|
times_min) = op
|
||||||
|
self.aux._do_line(
|
||||||
|
steps, length_mm, max_accel, max_jerk,
|
||||||
|
entry_vel, exit_vel, times_min,
|
||||||
|
ignore_limits=True)
|
||||||
elif kind == 'home':
|
elif kind == 'home':
|
||||||
self.aux.home()
|
self.aux.home()
|
||||||
# _pos_mm and DRO updated by the caller's enqueue.
|
# _pos_mm and DRO updated by the caller's enqueue.
|
||||||
|
|||||||
@@ -109,9 +109,13 @@ class FileHandler(bbctrl.APIHandler):
|
|||||||
try:
|
try:
|
||||||
from bbctrl.AuxPreprocessor import preprocess_file
|
from bbctrl.AuxPreprocessor import preprocess_file
|
||||||
log = self.get_log('AuxPreprocessor')
|
log = self.get_log('AuxPreprocessor')
|
||||||
if preprocess_file(filename.decode('utf8'), log=log):
|
ext = getattr(self.get_ctrl(), 'ext_axis', None)
|
||||||
log.info('Rewrote ATC M-codes in %s' %
|
coupling = (ext.coupling_for_preprocessor()
|
||||||
self.uploadFilename)
|
if ext is not None else None)
|
||||||
|
if preprocess_file(filename.decode('utf8'),
|
||||||
|
log=log, coupling=coupling):
|
||||||
|
log.info('Rewrote upload (ATC / Z-A coupling) in %s'
|
||||||
|
% self.uploadFilename)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.get_log('AuxPreprocessor').exception(
|
self.get_log('AuxPreprocessor').exception(
|
||||||
'Aux preprocess failed; uploading unchanged')
|
'Aux preprocess failed; uploading unchanged')
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ class Mach(Comm):
|
|||||||
self.planner = bbctrl.Planner(ctrl)
|
self.planner = bbctrl.Planner(ctrl)
|
||||||
self.unpausing = False
|
self.unpausing = False
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
|
# Guard against overlapping deferred-external-homing threads
|
||||||
|
# if the user clicks Home (All) again while the previous run
|
||||||
|
# is still waiting for the AVR cycle to finish.
|
||||||
|
self._ext_home_thread = None
|
||||||
|
|
||||||
ctrl.state.set('cycle', 'idle')
|
ctrl.state.set('cycle', 'idle')
|
||||||
|
|
||||||
@@ -327,6 +331,16 @@ class Mach(Comm):
|
|||||||
axes = 'zxybc' if is_rotary_active else 'zxyabc' # TODO This should be configurable
|
axes = 'zxybc' if is_rotary_active else 'zxyabc' # TODO This should be configurable
|
||||||
else: axes = '%c' % axis
|
else: axes = '%c' % axis
|
||||||
|
|
||||||
|
# Collect external axes here and process them *after* every
|
||||||
|
# AVR axis above has finished its homing cycle. Without this,
|
||||||
|
# the AVR is still running Z/X/Y homing G-code in the
|
||||||
|
# planner queue while ext.home() synchronously drives the ESP
|
||||||
|
# to home A in parallel - which is unsafe (the gantry and W
|
||||||
|
# axis can move at the same time) and visually confusing.
|
||||||
|
# We defer external homing to a background thread that
|
||||||
|
# polls cycle until the AVR cycle completes.
|
||||||
|
external_pending = []
|
||||||
|
|
||||||
for axis in axes:
|
for axis in axes:
|
||||||
enabled = state.is_axis_enabled(axis)
|
enabled = state.is_axis_enabled(axis)
|
||||||
mode = state.axis_homing_mode(axis)
|
mode = state.axis_homing_mode(axis)
|
||||||
@@ -334,27 +348,33 @@ class Mach(Comm):
|
|||||||
# External axes (e.g. the auxcnc-driven A axis) home via
|
# External axes (e.g. the auxcnc-driven A axis) home via
|
||||||
# their own ESP-side homing routine; the standard
|
# their own ESP-side homing routine; the standard
|
||||||
# G28.2 / G38.6 / latch sequence doesn't apply.
|
# G28.2 / G38.6 / latch sequence doesn't apply.
|
||||||
|
#
|
||||||
|
# After homing we want a deterministic outcome regardless
|
||||||
|
# of where the user was before:
|
||||||
|
# physical position = home_position_mm (e.g. 134 mm)
|
||||||
|
# work-coord origin = home position (user A = 0)
|
||||||
|
# work offset = home_position_mm (so abs - off = 0)
|
||||||
|
#
|
||||||
|
# ext.home() blocks on the ESP and updates state.ap to
|
||||||
|
# home_position_mm. We then need to tell the AVR (so its
|
||||||
|
# ex.position[A] matches physical reality) and gplan
|
||||||
|
# (so trajectory planning sees abs at home).
|
||||||
|
#
|
||||||
|
# We deliberately avoid G28.3 here: gplan's G28.3 keeps the
|
||||||
|
# current user-coord position fixed and adjusts the offset
|
||||||
|
# to match the new abs, which means re-homing after a move
|
||||||
|
# accumulates offset (134 -> 268 -> ...). Using G92 a0
|
||||||
|
# *after* syncing abs gives the desired "user A = 0 here"
|
||||||
|
# outcome with offset = home_position every time.
|
||||||
ext = getattr(self.ctrl, 'ext_axis', None)
|
ext = getattr(self.ctrl, 'ext_axis', None)
|
||||||
if ext is not None and ext.enabled \
|
if ext is not None and ext.enabled \
|
||||||
and ext.axis_letter == axis.lower():
|
and ext.axis_letter == axis.lower():
|
||||||
if 1 < len(axes) and not enabled:
|
if 1 < len(axes) and not enabled:
|
||||||
continue
|
continue
|
||||||
self.mlog.info('Homing external %s axis via auxcnc' %
|
# Defer until AVR axes are done. We capture the axis
|
||||||
axis.upper())
|
# letter and ext reference; the actual homing runs
|
||||||
# ext.home() blocks on the ESP. Use the lower-level
|
# in _run_external_homing below.
|
||||||
# planner.mdi (not Mach.mdi) so we don't try to
|
external_pending.append((axis, ext))
|
||||||
# _begin_cycle('mdi') from inside the home-all loop
|
|
||||||
# which is already in the 'homing' cycle.
|
|
||||||
try:
|
|
||||||
self._begin_cycle('homing')
|
|
||||||
ext.home()
|
|
||||||
self.planner.mdi(
|
|
||||||
'G28.3 %c%f' % (axis, ext.home_position_mm),
|
|
||||||
False)
|
|
||||||
super().resume()
|
|
||||||
except Exception as e:
|
|
||||||
self.mlog.error(
|
|
||||||
'External axis homing failed: %s' % e)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# If this is not a request to home a specific axis and the
|
# If this is not a request to home a specific axis and the
|
||||||
@@ -387,6 +407,122 @@ class Mach(Comm):
|
|||||||
self.planner.mdi(gcode, False)
|
self.planner.mdi(gcode, False)
|
||||||
super().resume()
|
super().resume()
|
||||||
|
|
||||||
|
# Kick off the deferred external-axis homing on a background
|
||||||
|
# thread so we don't block the HTTP handler (which is on the
|
||||||
|
# IOLoop) waiting for the AVR cycle to finish.
|
||||||
|
if external_pending:
|
||||||
|
prev = self._ext_home_thread
|
||||||
|
if prev is not None and prev.is_alive():
|
||||||
|
self.mlog.info(
|
||||||
|
'External homing already in progress; ignoring '
|
||||||
|
'duplicate request')
|
||||||
|
else:
|
||||||
|
import threading
|
||||||
|
t = threading.Thread(
|
||||||
|
target=self._run_external_homing,
|
||||||
|
args=(list(external_pending),),
|
||||||
|
name='ext-home-deferred',
|
||||||
|
daemon=True)
|
||||||
|
self._ext_home_thread = t
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def _run_external_homing(self, pending):
|
||||||
|
"""Background worker: wait for the AVR cycle to drop to idle
|
||||||
|
(meaning all queued AVR-side homing is done), then run each
|
||||||
|
deferred external-axis home in order.
|
||||||
|
|
||||||
|
We split the work between two threads:
|
||||||
|
- this background thread blocks on the ESP serial RPC
|
||||||
|
(ext.home(), which can take 5-10 seconds while the
|
||||||
|
carriage seeks the limit and backs off twice);
|
||||||
|
- small bookkeeping operations that touch gplan, the AVR
|
||||||
|
command queue, or shared State are scheduled back onto
|
||||||
|
the IOLoop via ctrl.ioloop.add_callback() so we don't
|
||||||
|
race with the rest of the controller.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
# Wait up to 5 minutes for the AVR cycle to leave 'homing'.
|
||||||
|
# Long enough for any reasonable Onefinity full-travel home
|
||||||
|
# (Y axis at slow rate covers ~800 mm).
|
||||||
|
deadline = time.time() + 300.0
|
||||||
|
while time.time() < deadline:
|
||||||
|
cycle = self._get_cycle()
|
||||||
|
# 'homing' is the AVR's homing cycle; we wait for it to
|
||||||
|
# return to idle. If the user estopped or the cycle was
|
||||||
|
# aborted, cycle goes to idle too - we still proceed and
|
||||||
|
# the external home will fail-soft if conditions are wrong.
|
||||||
|
if cycle == 'idle':
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
self.mlog.error(
|
||||||
|
'External axis homing aborted: AVR cycle did not '
|
||||||
|
'return to idle within timeout')
|
||||||
|
return
|
||||||
|
|
||||||
|
for axis, ext in pending:
|
||||||
|
self.mlog.info('Homing external %s axis via auxcnc' %
|
||||||
|
axis.upper())
|
||||||
|
# Begin the cycle on the IOLoop so cycle-state writes go
|
||||||
|
# through the same thread that all other state writes do.
|
||||||
|
self.ctrl.ioloop.add_callback(self._begin_cycle, 'homing')
|
||||||
|
try:
|
||||||
|
# ext.home() runs on this background thread - it
|
||||||
|
# blocks on serial I/O and is fully thread-safe (the
|
||||||
|
# AuxAxis driver has its own RPC lock).
|
||||||
|
ext.home()
|
||||||
|
home_mm = ext.home_position_mm
|
||||||
|
# All of the post-home bookkeeping touches gplan and
|
||||||
|
# the AVR command queue, both of which run on the
|
||||||
|
# IOLoop. Schedule it there in a single callback so
|
||||||
|
# the steps run in order without intervening events.
|
||||||
|
self.ctrl.ioloop.add_callback(
|
||||||
|
self._finish_external_home, axis, home_mm)
|
||||||
|
except Exception as e:
|
||||||
|
self.mlog.error(
|
||||||
|
'External axis homing failed: %s' % e)
|
||||||
|
# Cycle reset must also happen on the IOLoop. Without
|
||||||
|
# this the UI stays locked at 'homing' since the AVR
|
||||||
|
# never moved (no state change to drive _update's
|
||||||
|
# cycle-end path).
|
||||||
|
self.ctrl.ioloop.add_callback(
|
||||||
|
self._abort_external_home_cycle)
|
||||||
|
|
||||||
|
def _finish_external_home(self, axis, home_mm):
|
||||||
|
"""IOLoop-side completion of an external axis home.
|
||||||
|
Synchronizes AVR position, refreshes the planner, and emits
|
||||||
|
a G92 to set the user-coord origin at the home position.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 1) Update AVR: no motor steps, just position sync.
|
||||||
|
super().queue_command(Cmd.set_axis(axis, home_mm))
|
||||||
|
# 2) Force planner to resync abs from State on the next
|
||||||
|
# planner call (which is the MDI below).
|
||||||
|
self.planner.position_change()
|
||||||
|
# 3) G92 <axis>0: with abs already at home_mm, sets
|
||||||
|
# user-coord A = 0 and offset = home_mm. Use
|
||||||
|
# planner.mdi (not Mach.mdi) so we don't flip cycle
|
||||||
|
# to 'mdi' inside the 'homing' cycle.
|
||||||
|
self.planner.mdi('G92 %c0' % axis, False)
|
||||||
|
super().resume()
|
||||||
|
except Exception:
|
||||||
|
self.mlog.exception(
|
||||||
|
'Post-home bookkeeping failed for external axis')
|
||||||
|
self._abort_external_home_cycle()
|
||||||
|
|
||||||
|
def _abort_external_home_cycle(self):
|
||||||
|
"""Reset cycle to idle from the IOLoop after a failed
|
||||||
|
external axis home. The AVR never moved so _update's normal
|
||||||
|
cycle-end path won't fire; do it explicitly here.
|
||||||
|
"""
|
||||||
|
if self._get_cycle() == 'homing':
|
||||||
|
try:
|
||||||
|
self._set_cycle('idle')
|
||||||
|
except Exception:
|
||||||
|
self.mlog.exception(
|
||||||
|
'Failed to reset cycle to idle after external '
|
||||||
|
'homing error')
|
||||||
|
|
||||||
|
|
||||||
def unhome(self, axis):
|
def unhome(self, axis):
|
||||||
# External axes don't have AVR-side homed state to clear; the
|
# External axes don't have AVR-side homed state to clear; the
|
||||||
|
|||||||
@@ -270,19 +270,54 @@ class Planner():
|
|||||||
if type != 'set': self.log.info('Cmd:' + log_json(block))
|
if type != 'set': self.log.info('Cmd:' + log_json(block))
|
||||||
|
|
||||||
if type == 'line':
|
if type == 'line':
|
||||||
|
# Z-A coupling check: every line block that touches Z (or
|
||||||
|
# A) is validated against the projected (A,Z) machine
|
||||||
|
# pair. The ExternalAxis check is improvement-aware: it
|
||||||
|
# only refuses moves that worsen an existing violation
|
||||||
|
# or push a healthy state into one. So pure-XY jogs and
|
||||||
|
# recovery moves (Z up, A down) are not rejected even
|
||||||
|
# when (A-Z) is currently above the bound.
|
||||||
|
ext_check = getattr(self.ctrl, 'ext_axis', None)
|
||||||
|
if ext_check is not None:
|
||||||
|
from bbctrl.ExternalAxis import ExternalAxisError
|
||||||
|
target = block.get('target') or {}
|
||||||
|
z_target = target.get('z')
|
||||||
|
if z_target is None: z_target = target.get('Z')
|
||||||
|
a_letter = ext_check.axis_letter
|
||||||
|
a_target = target.get(a_letter)
|
||||||
|
if a_target is None:
|
||||||
|
a_target = target.get(a_letter.upper())
|
||||||
|
if z_target is not None or a_target is not None:
|
||||||
|
try:
|
||||||
|
ext_check.check_coupling(
|
||||||
|
target_a_machine=a_target,
|
||||||
|
target_z_machine=z_target)
|
||||||
|
except ExternalAxisError as e:
|
||||||
|
# Convert the raw error into a clean abort:
|
||||||
|
# surface the message to the operator, stop
|
||||||
|
# the cycle, and skip this block. Returning
|
||||||
|
# None drops the block from the AVR queue;
|
||||||
|
# mach.stop() halts further planner output
|
||||||
|
# so the rest of an offending program can't
|
||||||
|
# leak through. The planner stays usable
|
||||||
|
# for new MDI / jog commands.
|
||||||
|
self.log.warning('Z-A coupling refused: %s' % e)
|
||||||
|
try:
|
||||||
|
self.ctrl.state.add_message(
|
||||||
|
'Z-A coupling refused move: ' + str(e))
|
||||||
|
except Exception: pass
|
||||||
|
try:
|
||||||
|
self.ctrl.mach.stop()
|
||||||
|
except Exception: pass
|
||||||
|
return None
|
||||||
|
|
||||||
ext = self._external_axis_for_line(block)
|
ext = self._external_axis_for_line(block)
|
||||||
if ext is not None:
|
if ext is not None:
|
||||||
# Side-effects: run the ESP move synchronously,
|
# Side effect: enqueue the ESP move on the external-
|
||||||
# split the line into ESP (already done) + AVR (rest).
|
# axis worker. The AVR still receives the full target
|
||||||
avr_block = self._dispatch_external_line(block, ext)
|
# (including A) so ex.position[A] tracks gplan; no
|
||||||
if avr_block is None:
|
# motor steps for A because no motor maps to it.
|
||||||
# Pure external move - no AVR work to issue but
|
self._dispatch_external_line(block, ext)
|
||||||
# we still need to ack the block id so the planner
|
|
||||||
# advances. CommandQueue.enqueue with no callback
|
|
||||||
# at block id is what _encode does, so return an
|
|
||||||
# empty cmd to short-circuit there.
|
|
||||||
return ''
|
|
||||||
block = avr_block
|
|
||||||
self._enqueue_line_time(block)
|
self._enqueue_line_time(block)
|
||||||
return Cmd.line(block['target'], block['exit-vel'],
|
return Cmd.line(block['target'], block['exit-vel'],
|
||||||
block['max-accel'], block['max-jerk'],
|
block['max-accel'], block['max-jerk'],
|
||||||
@@ -394,34 +429,45 @@ class Planner():
|
|||||||
|
|
||||||
def _dispatch_external_line(self, block, ext):
|
def _dispatch_external_line(self, block, ext):
|
||||||
"""Side-effect: enqueue the ESP move on the external-axis
|
"""Side-effect: enqueue the ESP move on the external-axis
|
||||||
worker thread (non-blocking). Return a new block dict with
|
worker thread (non-blocking). Returns the block (possibly
|
||||||
the external axis stripped from `target`, or None if the
|
unchanged) for the AVR.
|
||||||
line had no other axes.
|
|
||||||
|
|
||||||
For mixed XYZ + external moves the AVR runs XYZ at the
|
We do NOT strip the external axis target from the AVR line.
|
||||||
gplan-computed rate while the ESP runs the external delta in
|
The AVR's exec_move_to_target updates ex.position[axis] for
|
||||||
parallel. Pure external moves return None so __encode emits
|
every axis in the target dict regardless of motor mapping,
|
||||||
only the id-sync to keep planner ids advancing."""
|
and reports it back via the `p` indexed var. Leaving A in
|
||||||
target = dict(block['target'])
|
the target keeps state.ap in sync with gplan's idea of A
|
||||||
new_target, ext_mm = ext.split_target(target)
|
(otherwise the AVR's stale ex.position[A] would clobber
|
||||||
|
ExternalAxis's state.ap=N update on the next status report).
|
||||||
|
|
||||||
|
The AVR doesn't step any motor for the external axis (no
|
||||||
|
motor maps to it) - so leaving A in the target is
|
||||||
|
physically a no-op for the steppers, while keeping the
|
||||||
|
host-side state coherent.
|
||||||
|
|
||||||
|
We pass the full S-curve parameters to the ESP so its move
|
||||||
|
duration matches the AVR's exactly. The ESP runs the same
|
||||||
|
7-segment jerk-limited trajectory the AVR would have run
|
||||||
|
if A had been a real motor."""
|
||||||
|
target = block.get('target') or {}
|
||||||
|
# Read the external target (case-insensitive) without modifying
|
||||||
|
# the dict so the AVR still sees A.
|
||||||
|
ext_mm = target.get(ext.axis_letter)
|
||||||
|
if ext_mm is None:
|
||||||
|
ext_mm = target.get(ext.axis_letter.upper())
|
||||||
try:
|
try:
|
||||||
ext.enqueue_target_mm(ext_mm)
|
ext.enqueue_line(
|
||||||
|
ext_mm,
|
||||||
|
block.get('max-accel', 0.0),
|
||||||
|
block.get('max-jerk', 0.0),
|
||||||
|
block.get('entry-vel', 0.0),
|
||||||
|
block.get('exit-vel', 0.0),
|
||||||
|
block.get('times', [0]*7),
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Non-blocking enqueue should rarely fail; if it does we
|
|
||||||
# still want the planner to stop so the user notices.
|
|
||||||
self.log.error('External axis enqueue failed: %s' % e)
|
self.log.error('External axis enqueue failed: %s' % e)
|
||||||
raise
|
raise
|
||||||
|
return block
|
||||||
if not new_target:
|
|
||||||
# Pure external move; nothing left for the AVR. Track the
|
|
||||||
# trajectory time so the planner's plan_time stays correct.
|
|
||||||
self._enqueue_line_time(block)
|
|
||||||
return None
|
|
||||||
# Build a clean copy with only the AVR axes left.
|
|
||||||
avr_block = dict(block)
|
|
||||||
avr_block['target'] = new_target
|
|
||||||
return avr_block
|
|
||||||
|
|
||||||
def reset(self, *args, **kwargs):
|
def reset(self, *args, **kwargs):
|
||||||
stop = kwargs.get('stop', True)
|
stop = kwargs.get('stop', True)
|
||||||
@@ -471,8 +517,11 @@ class Planner():
|
|||||||
# should use A directly.
|
# should use A directly.
|
||||||
try:
|
try:
|
||||||
from bbctrl.AuxPreprocessor import preprocess_file
|
from bbctrl.AuxPreprocessor import preprocess_file
|
||||||
if preprocess_file(path, log = self.log):
|
ext = getattr(self.ctrl, 'ext_axis', None)
|
||||||
self.log.info('Rewrote ATC M-codes in %s' % path)
|
coupling = (ext.coupling_for_preprocessor()
|
||||||
|
if ext is not None else None)
|
||||||
|
if preprocess_file(path, log=self.log, coupling=coupling):
|
||||||
|
self.log.info('Rewrote (ATC / Z-A coupling) in %s' % path)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception('Aux preprocess at load failed; '
|
self.log.exception('Aux preprocess at load failed; '
|
||||||
'attempting to load file unchanged')
|
'attempting to load file unchanged')
|
||||||
|
|||||||
@@ -107,8 +107,14 @@ class State(object):
|
|||||||
|
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
# Unhome all motors
|
# Unhome all motors (real AVR motors 0..3 and the synthetic
|
||||||
for i in range(4): self.set('%dhomed' % i, False)
|
# external-axis motor at index 4 used by ExternalAxis).
|
||||||
|
# Both <motor>homed and <motor>h are cleared - they're set
|
||||||
|
# by different code paths (gplan emits homed via _<axis>_homed
|
||||||
|
# set blocks, AVR reports h directly).
|
||||||
|
for i in range(5):
|
||||||
|
self.set('%dhomed' % i, False)
|
||||||
|
self.set('%dh' % i, 0)
|
||||||
|
|
||||||
# Zero offsets and positions
|
# Zero offsets and positions
|
||||||
for axis in 'xyzabc':
|
for axis in 'xyzabc':
|
||||||
|
|||||||
@@ -1396,7 +1396,7 @@ tt.save
|
|||||||
|
|
||||||
.control-page .dro-head, .control-page .dro-row
|
.control-page .dro-head, .control-page .dro-row
|
||||||
display grid
|
display grid
|
||||||
grid-template-columns 84px 1.4fr 1fr 1fr 170px 170px 280px
|
grid-template-columns 84px 1.4fr 1fr 1fr 280px
|
||||||
column-gap 0.75rem
|
column-gap 0.75rem
|
||||||
align-items center
|
align-items center
|
||||||
padding 14px 22px
|
padding 14px 22px
|
||||||
@@ -1529,6 +1529,39 @@ tt.save
|
|||||||
opacity 0.45
|
opacity 0.45
|
||||||
cursor not-allowed
|
cursor not-allowed
|
||||||
|
|
||||||
|
// State-tinted variants used on home + zero buttons in DRO rows
|
||||||
|
// to communicate per-axis homing / toolpath fit at a glance,
|
||||||
|
// replacing the explicit HOMED / OK chips that used to live in
|
||||||
|
// their own columns.
|
||||||
|
&.state-green
|
||||||
|
background #dcfce7
|
||||||
|
border-color #86efac
|
||||||
|
color #166534
|
||||||
|
|
||||||
|
&:hover:not([disabled])
|
||||||
|
background #bbf7d0
|
||||||
|
|
||||||
|
&.state-amber
|
||||||
|
background #fef3c7
|
||||||
|
border-color #fcd34d
|
||||||
|
color #92400e
|
||||||
|
|
||||||
|
&:hover:not([disabled])
|
||||||
|
background #fde68a
|
||||||
|
|
||||||
|
&.state-red
|
||||||
|
background #fee2e2
|
||||||
|
border-color #fca5a5
|
||||||
|
color #991b1b
|
||||||
|
|
||||||
|
&:hover:not([disabled])
|
||||||
|
background #fecaca
|
||||||
|
|
||||||
|
&[disabled].state-green,
|
||||||
|
&[disabled].state-amber,
|
||||||
|
&[disabled].state-red
|
||||||
|
opacity 0.7
|
||||||
|
|
||||||
.actions-cell
|
.actions-cell
|
||||||
display flex
|
display flex
|
||||||
justify-content flex-end
|
justify-content flex-end
|
||||||
@@ -2380,7 +2413,7 @@ html.kiosk-mode
|
|||||||
gap 6px
|
gap 6px
|
||||||
|
|
||||||
.control-page .dro-head, .control-page .dro-row
|
.control-page .dro-head, .control-page .dro-row
|
||||||
grid-template-columns 56px 1fr 0.85fr 0.85fr 90px 90px 1fr
|
grid-template-columns 56px 1fr 0.85fr 0.85fr 1fr
|
||||||
column-gap 0.4rem
|
column-gap 0.4rem
|
||||||
padding 6px 10px
|
padding 6px 10px
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
import * as api from "$lib/api";
|
import * as api from "$lib/api";
|
||||||
|
|
||||||
// Mirrors the DEFAULTS in src/py/bbctrl/AuxAxis.py. The "enabled"
|
// Mirrors the DEFAULTS in src/py/bbctrl/AuxAxis.py. The "enabled"
|
||||||
// flag is read-only here; toggling the W axis on/off is done via
|
// flag is read-only here; toggling the auxiliary A axis on/off
|
||||||
// aux.json on disk, so adding/removing the hardware doesn't have a
|
// is done via aux.json on disk, so adding/removing the hardware
|
||||||
// surprise UI that bricks bring-up.
|
// doesn't have a surprise UI that bricks bring-up. Legacy aux.json
|
||||||
|
// files using min_w/max_w are migrated up to min_mm/max_mm by
|
||||||
|
// AuxAxis._migrate_legacy_fields on load.
|
||||||
type AuxConfig = {
|
type AuxConfig = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
port: string;
|
port: string;
|
||||||
@@ -14,8 +16,8 @@
|
|||||||
steps_per_mm: number;
|
steps_per_mm: number;
|
||||||
dir_sign: number;
|
dir_sign: number;
|
||||||
axis_letter: string;
|
axis_letter: string;
|
||||||
min_w: number;
|
min_mm: number;
|
||||||
max_w: number;
|
max_mm: number;
|
||||||
max_feed_mm_min: number;
|
max_feed_mm_min: number;
|
||||||
max_velocity_m_per_min: number;
|
max_velocity_m_per_min: number;
|
||||||
max_accel_km_per_min2: number;
|
max_accel_km_per_min2: number;
|
||||||
@@ -30,6 +32,9 @@
|
|||||||
step_accel_sps2: number;
|
step_accel_sps2: number;
|
||||||
step_start_sps: number;
|
step_start_sps: number;
|
||||||
limit_low: boolean;
|
limit_low: boolean;
|
||||||
|
couple_z_enabled: boolean;
|
||||||
|
couple_z_clearance_mm: number;
|
||||||
|
z_home_mm: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
let cfg: AuxConfig | null = null;
|
let cfg: AuxConfig | null = null;
|
||||||
@@ -73,9 +78,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the root config as modified whenever a W axis field is
|
// Mark the root config as modified whenever an auxiliary axis
|
||||||
// edited, so the master Save button highlights and the user knows
|
// field is edited, so the master Save button highlights and
|
||||||
// there are unsaved changes.
|
// the user knows there are unsaved changes.
|
||||||
function markDirty() {
|
function markDirty() {
|
||||||
try {
|
try {
|
||||||
const root = (window as any).$root || (window as any).Vue?.root;
|
const root = (window as any).$root || (window as any).Vue?.root;
|
||||||
@@ -86,9 +91,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-axis-settings">
|
<div class="a-axis-settings">
|
||||||
{#if !cfg}
|
{#if !cfg}
|
||||||
<p class="tip">Loading W axis configuration...</p>
|
<p class="tip">Loading A axis configuration...</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="status">
|
<div class="status">
|
||||||
{#if status}
|
{#if status}
|
||||||
@@ -109,7 +114,7 @@
|
|||||||
|
|
||||||
<div class="pure-form pure-form-aligned" on:input={markDirty} on:change={markDirty}>
|
<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 auxiliary axis (auxcnc-driven A). Edit aux.json to toggle.">
|
||||||
<label for="enabled">enabled</label>
|
<label for="enabled">enabled</label>
|
||||||
<input id="enabled" type="checkbox" checked={cfg.enabled} disabled />
|
<input id="enabled" type="checkbox" checked={cfg.enabled} disabled />
|
||||||
<label for="" class="units">(edit aux.json)</label>
|
<label for="" class="units">(edit aux.json)</label>
|
||||||
@@ -128,7 +133,7 @@
|
|||||||
|
|
||||||
<h3>Mechanics</h3>
|
<h3>Mechanics</h3>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<div class="pure-control-group" title="Logical steps per mm of W travel.">
|
<div class="pure-control-group" title="Logical steps per mm of axis travel.">
|
||||||
<label for="steps_per_mm">steps per mm</label>
|
<label for="steps_per_mm">steps per mm</label>
|
||||||
<input id="steps_per_mm" type="number" bind:value={cfg.steps_per_mm} step="any" />
|
<input id="steps_per_mm" type="number" bind:value={cfg.steps_per_mm} step="any" />
|
||||||
<label for="" class="units">steps/mm</label>
|
<label for="" class="units">steps/mm</label>
|
||||||
@@ -152,14 +157,43 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group" title="Soft-limit minimum in mm.">
|
<div class="pure-control-group" title="Soft-limit minimum in mm.">
|
||||||
<label for="min_w">soft min</label>
|
<label for="min_mm">soft min</label>
|
||||||
<input id="min_w" type="number" bind:value={cfg.min_w} step="any" />
|
<input id="min_mm" type="number" bind:value={cfg.min_mm} step="any" />
|
||||||
<label for="" class="units">mm</label>
|
<label for="" class="units">mm</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group" title="Soft-limit maximum in mm.">
|
<div class="pure-control-group" title="Soft-limit maximum in mm.">
|
||||||
<label for="max_w">soft max</label>
|
<label for="max_mm">soft max</label>
|
||||||
<input id="max_w" type="number" bind:value={cfg.max_w} step="any" />
|
<input id="max_mm" type="number" bind:value={cfg.max_mm} step="any" />
|
||||||
|
<label for="" class="units">mm</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<h3>Z-A Coupling</h3>
|
||||||
|
<p class="tip">
|
||||||
|
The auxiliary tool hangs below the Z spindle. Beyond a small
|
||||||
|
Z descent the two collide unless A drops with Z. The rule
|
||||||
|
in machine coordinates is
|
||||||
|
<code>A − Z ≤ (A_home − Z_home) + clearance</code>.
|
||||||
|
When enabled, the planner refuses moves that would violate
|
||||||
|
it and the gcode preprocessor injects pre-position A moves
|
||||||
|
into uploaded files.
|
||||||
|
</p>
|
||||||
|
<fieldset>
|
||||||
|
<div class="pure-control-group" title="Master switch for the Z-A interlock. When off, no checks are performed.">
|
||||||
|
<label for="couple_z_enabled">enable coupling</label>
|
||||||
|
<input id="couple_z_enabled" type="checkbox" bind:checked={cfg.couple_z_enabled} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="How far Z may descend below its home position before A must move with it.">
|
||||||
|
<label for="couple_z_clearance_mm">Z clearance</label>
|
||||||
|
<input id="couple_z_clearance_mm" type="number" bind:value={cfg.couple_z_clearance_mm} step="any" />
|
||||||
|
<label for="" class="units">mm</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group" title="Z's machine position when homed. Almost always 0.">
|
||||||
|
<label for="z_home_mm">Z home position</label>
|
||||||
|
<input id="z_home_mm" type="number" bind:value={cfg.z_home_mm} step="any" />
|
||||||
<label for="" class="units">mm</label>
|
<label for="" class="units">mm</label>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -196,12 +230,12 @@
|
|||||||
<div class="pure-control-group" title="Direction the axis moves when looking for the home limit switch.">
|
<div class="pure-control-group" title="Direction the axis moves when looking for the home limit switch.">
|
||||||
<label for="home_dir">home direction</label>
|
<label for="home_dir">home direction</label>
|
||||||
<select id="home_dir" bind:value={cfg.home_dir}>
|
<select id="home_dir" bind:value={cfg.home_dir}>
|
||||||
<option value="-">- (toward W-)</option>
|
<option value="-">- (toward A-)</option>
|
||||||
<option value="+">+ (toward W+)</option>
|
<option value="+">+ (toward A+)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group" title="W position assigned when homing completes.">
|
<div class="pure-control-group" title="Axis position assigned when homing completes.">
|
||||||
<label for="home_position_mm">home position</label>
|
<label for="home_position_mm">home position</label>
|
||||||
<input id="home_position_mm" type="number" bind:value={cfg.home_position_mm} step="any" />
|
<input id="home_position_mm" type="number" bind:value={cfg.home_position_mm} step="any" />
|
||||||
<label for="" class="units">mm</label>
|
<label for="" class="units">mm</label>
|
||||||
@@ -263,7 +297,7 @@
|
|||||||
master <strong>Save</strong> button at the bottom of the
|
master <strong>Save</strong> button at the bottom of the
|
||||||
settings rail. Homing rates and the limit polarity are
|
settings rail. Homing rates and the limit polarity are
|
||||||
pushed to the ESP immediately; any running motion is
|
pushed to the ESP immediately; any running motion is
|
||||||
unaffected. Re-home the W axis after changing direction,
|
unaffected. Re-home the auxiliary axis after changing direction,
|
||||||
sign, or step settings.
|
sign, or step settings.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,7 +305,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.w-axis-settings {
|
.a-axis-settings {
|
||||||
.status {
|
.status {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
import configTemplate from "../../../resources/config-template.json";
|
import configTemplate from "../../../resources/config-template.json";
|
||||||
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
|
import ScreenRotationDialog from "$dialogs/ScreenRotationDialog.svelte";
|
||||||
import ConfigTemplatedInput from "./ConfigTemplatedInput.svelte";
|
import ConfigTemplatedInput from "./ConfigTemplatedInput.svelte";
|
||||||
// WAxisSettings is mounted directly by the V09 settings shell at
|
// AAxisSettings is mounted directly by the V09 settings shell at
|
||||||
// #w-axis instead of being embedded here — see
|
// #a-axis instead of being embedded here — see
|
||||||
// src/pug/templates/w-axis-view.pug.
|
// src/pug/templates/a-axis-view.pug.
|
||||||
// import WAxisSettings from "./WAxisSettings.svelte";
|
// import AAxisSettings from "./AAxisSettings.svelte";
|
||||||
import SetTimeDialog from "$dialogs/SetTimeDialog.svelte";
|
import SetTimeDialog from "$dialogs/SetTimeDialog.svelte";
|
||||||
import Button, { Label } from "@smui/button";
|
import Button, { Label } from "@smui/button";
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- W Axis (auxcnc) is now its own routed page in the V09
|
<!-- W Axis (auxcnc) is now its own routed page in the V09
|
||||||
settings shell (#w-axis). Keep the SettingsView free of
|
settings shell (#a-axis). Keep the SettingsView free of
|
||||||
that section so we don't render it twice. -->
|
that section so we don't render it twice. -->
|
||||||
|
|
||||||
<h2 id="sec-path-accuracy" data-sec="gcode">Path Accuracy</h2>
|
<h2 id="sec-path-accuracy" data-sec="gcode">Path Accuracy</h2>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ matchAll.shim();
|
|||||||
import AdminNetworkView from "$components/AdminNetworkView.svelte";
|
import AdminNetworkView from "$components/AdminNetworkView.svelte";
|
||||||
import SettingsView from "$components/SettingsView.svelte";
|
import SettingsView from "$components/SettingsView.svelte";
|
||||||
import HelpView from "$components/HelpView.svelte";
|
import HelpView from "$components/HelpView.svelte";
|
||||||
import WAxisSettings from "$components/WAxisSettings.svelte";
|
import AAxisSettings from "$components/AAxisSettings.svelte";
|
||||||
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
|
import DialogHost, { showDialog } from "$dialogs/DialogHost.svelte";
|
||||||
import { handleConfigUpdate, setDisplayUnits } from "$lib/ConfigStore";
|
import { handleConfigUpdate, setDisplayUnits } from "$lib/ConfigStore";
|
||||||
import { handleControllerStateUpdate } from "$lib/ControllerState";
|
import { handleControllerStateUpdate } from "$lib/ControllerState";
|
||||||
@@ -23,8 +23,8 @@ export function createComponent(component: string, target: HTMLElement, props: R
|
|||||||
case "HelpView":
|
case "HelpView":
|
||||||
return new HelpView({ target, props });
|
return new HelpView({ target, props });
|
||||||
|
|
||||||
case "WAxisSettings":
|
case "AAxisSettings":
|
||||||
return new WAxisSettings({ target, props });
|
return new AAxisSettings({ target, props });
|
||||||
|
|
||||||
case "DialogHost":
|
case "DialogHost":
|
||||||
return new DialogHost({ target, props });
|
return new DialogHost({ target, props });
|
||||||
|
|||||||
Reference in New Issue
Block a user