ExternalAxis exposes the auxcnc-driven ESP stepper as motor 4 (a
synthetic, host-only motor that gplan sees but the AVR doesn't). The
result is a virtual A axis that is fully integrated with the planner:
G1 A25 F1500 schedules a coordinated S-curve and the ESP runs the
exact same 7-segment trajectory the AVR would have run if A were a
real motor.
- ExternalAxis.py: synthetic-motor state, S-curve LINE block forward
to the ESP, soft-limit enforcement, option-(b) homing (user A=0
at the home limit).
- State: walk motors 0..4 in find_motor; clear both homed and h on
reset; expose synthetic motor vars.
- axis-vars.js: motor-4 guard so the JS computed axis bindings don't
throw when motor 4 has no entry in config.motors; resolve motor_id
for the synthetic axis by scanning state['4an'].
- Ctrl: instantiate ExternalAxis after AuxAxis, share the axis_letter
setting, wire AuxAxis state observer.
- Web: route /api/aux/{home,jog,move} through ExternalAxis when it
is enabled so the DRO and synthetic-motor flags stay in sync.
364 lines
12 KiB
JavaScript
364 lines
12 KiB
JavaScript
"use strict";
|
|
|
|
module.exports = {
|
|
props: [ "state", "config" ],
|
|
|
|
computed: {
|
|
metric: function() {
|
|
return this.$root.display_units === "METRIC";
|
|
},
|
|
|
|
x: function() {
|
|
return this._compute_axis("x");
|
|
},
|
|
|
|
y: function() {
|
|
return this._compute_axis("y");
|
|
},
|
|
|
|
z: function() {
|
|
return this._compute_axis("z");
|
|
},
|
|
|
|
a: function() {
|
|
return this._compute_axis("a");
|
|
},
|
|
|
|
b: function() {
|
|
return this._compute_axis("b");
|
|
},
|
|
|
|
c: function() {
|
|
return this._compute_axis("c");
|
|
},
|
|
|
|
w: function() {
|
|
return this._compute_aux_axis();
|
|
},
|
|
|
|
axes: function() {
|
|
return this._compute_axes();
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
_convert_length: function(value) {
|
|
return this.metric
|
|
? value
|
|
: value / 25.4;
|
|
},
|
|
|
|
_length_str: function(value) {
|
|
return this._convert_length(value).toLocaleString() + (this.metric ? " mm" : " in");
|
|
},
|
|
|
|
_compute_axis: function(axis) {
|
|
const abs = this.state[`${axis}p`] || 0;
|
|
const off = this.state[`offset_${axis}`];
|
|
const motor_id = this._get_motor_id(axis);
|
|
// motor_id may be 4 for the synthetic external-axis motor;
|
|
// there is no entry for it in config.motors so guard with
|
|
// an empty object to avoid undefined property access.
|
|
const motor = (motor_id == -1
|
|
? {}
|
|
: (this.config.motors[motor_id] || {}));
|
|
const enabled = this._check_is_enabled(axis);
|
|
const homingMode = motor["homing-mode"];
|
|
const homed = this.state[`${motor_id}homed`];
|
|
const min = this.state[`${motor_id}tn`];
|
|
const max = this.state[`${motor_id}tm`];
|
|
const dim = max - min;
|
|
const pathMin = this.state[`path_min_${axis}`];
|
|
const pathMax = this.state[`path_max_${axis}`];
|
|
const pathDim = pathMax - pathMin;
|
|
const under = pathMin + off < min;
|
|
const over = max < pathMax + off;
|
|
let klass = `${homed ? "homed" : "unhomed"} axis-${axis}`;
|
|
let state = "UNHOMED";
|
|
let icon = "question-circle";
|
|
const fault = this.state[`${motor_id}df`] & 0x1f;
|
|
const shutdown = this.state.power_shutdown;
|
|
let title;
|
|
let ticon = "question-circle";
|
|
let tstate = "NO FILE";
|
|
let toolmsg;
|
|
let tklass = `${homed ? "homed" : "unhomed"} axis-${axis}`;
|
|
|
|
if (fault || shutdown) {
|
|
state = shutdown ? "SHUTDOWN" : "FAULT";
|
|
klass += " error";
|
|
icon = "exclamation-circle";
|
|
} else if (homed) {
|
|
state = "HOMED";
|
|
icon = "check-circle";
|
|
}
|
|
|
|
if (0 < dim && dim < pathDim) {
|
|
tstate = "NO FIT";
|
|
tklass += " error";
|
|
ticon = "ban";
|
|
} else {
|
|
if (over || under) {
|
|
tstate = over ? "OVER" : "UNDER";
|
|
tklass += " warn";
|
|
ticon = "exclamation-circle";
|
|
} else {
|
|
tstate = "OK";
|
|
ticon = "check-circle";
|
|
}
|
|
}
|
|
|
|
switch (state) {
|
|
case "UNHOMED":
|
|
title = "Click the home button to home axis.";
|
|
break;
|
|
|
|
case "HOMED":
|
|
title = "Axis successfuly homed.";
|
|
break;
|
|
|
|
case "FAULT":
|
|
title = [
|
|
`Motor driver fault. A potentially damaging electrical`,
|
|
`condition was detected and the motor driver was shutdown.`,
|
|
`Please power down the controller and check your motor cabling.`,
|
|
`See the "Motor Faults" table on the "Indicators" tab for more`,
|
|
`information.`,
|
|
].join(" ");
|
|
break;
|
|
|
|
case "SHUTDOWN":
|
|
title = [
|
|
`Motor power fault. All motors in shutdown.`,
|
|
`See the "Power Faults" table on the "Indicators" tab for more`,
|
|
`information. Reboot controller to reset.`
|
|
].join(" ");
|
|
break;
|
|
}
|
|
|
|
switch (tstate) {
|
|
case "OVER":
|
|
toolmsg = [
|
|
`Caution: The current tool path file would move`,
|
|
`${this._length_str(pathMax + off - max)}`,
|
|
`above axis limit with the current offset.`
|
|
].join(" ");
|
|
break;
|
|
|
|
case "UNDER":
|
|
toolmsg = [
|
|
`Caution: The current tool path file would move`,
|
|
`${this._length_str(min - pathMin - off)}`,
|
|
`below limit with the current offset.`
|
|
].join(" ");
|
|
break;
|
|
|
|
case "NO FIT":
|
|
toolmsg = [
|
|
`Warning: The current tool path dimensions`,
|
|
`(${this._length_str(pathDim)}) exceed axis dimensions`,
|
|
`(${this._length_str(dim)}) by ${this._length_str(pathDim - dim)}.`
|
|
].join(" ");
|
|
break;
|
|
|
|
default:
|
|
toolmsg = `Tool path ${axis} dimensions OK.`;
|
|
break;
|
|
}
|
|
|
|
return {
|
|
pos: Math.abs(abs - off) < 0.00001 ? 0 : abs - off,
|
|
abs: abs,
|
|
off: off,
|
|
min: min,
|
|
max: max,
|
|
dim: dim,
|
|
pathMin: pathMin,
|
|
pathMax: pathMax,
|
|
pathDim: pathDim,
|
|
motor: motor_id,
|
|
enabled: enabled,
|
|
homingMode: homingMode,
|
|
homed: homed,
|
|
klass: klass,
|
|
state: state,
|
|
icon: icon,
|
|
title: title,
|
|
ticon: ticon,
|
|
tstate: tstate,
|
|
toolmsg: toolmsg,
|
|
tklass: tklass
|
|
};
|
|
},
|
|
|
|
_get_motor_id: function(axis) {
|
|
for (let i = 0; i < this.config.motors.length; i++) {
|
|
const motor = this.config.motors[i];
|
|
// motor.axis can be undefined on initial load before
|
|
// config has streamed in. Guard so the computed does
|
|
// not throw and bubble a Vue warning into the console.
|
|
if (motor && typeof motor.axis === "string" &&
|
|
motor.axis.toLowerCase() == axis) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
// Synthetic external motor (index 4) used by ExternalAxis
|
|
// to expose the auxcnc ESP stepper as a virtual axis.
|
|
// Its `Nan` lives in state, not config.
|
|
const axes = { x: 0, y: 1, z: 2, a: 3, b: 4, c: 5 };
|
|
const wanted = axes[axis];
|
|
const extAn = this.state && this.state["4an"];
|
|
if (typeof wanted === "number" && typeof extAn === "number"
|
|
&& extAn === wanted) {
|
|
return 4;
|
|
}
|
|
|
|
return -1;
|
|
},
|
|
|
|
_check_is_enabled: function(axis){
|
|
// Prefer config.motors[i].axis (always present once the
|
|
// config has loaded). Fall back to the per-motor state
|
|
// `Nan` field, which is what the legacy UI used. This
|
|
// avoids hiding axis rows during the brief window after
|
|
// config has loaded but before the controller has pushed
|
|
// its first state delta.
|
|
const axes = { x: 0, y: 1, z: 2, a: 3 };
|
|
const wanted = axes[axis];
|
|
for (let i = 0; i < this.config.motors.length; i++) {
|
|
const motor = this.config.motors[i] || {};
|
|
if (typeof motor.axis === "string" &&
|
|
motor.axis.toLowerCase() == axis) {
|
|
return motor.enabled !== false;
|
|
}
|
|
// Only use the state Nan fallback for axes we know
|
|
// about (x/y/z/a). Otherwise undefined == undefined
|
|
// would mistakenly match every axis (b, c, ...).
|
|
if (typeof wanted === "number") {
|
|
const an = this.state[`${i}an`];
|
|
if (typeof an === "number" && an === wanted) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// Synthetic external motor (index 4) - the auxcnc ESP
|
|
// stepper exposed as A via ExternalAxis.
|
|
if (typeof wanted === "number") {
|
|
const extAn = this.state["4an"];
|
|
const extMe = this.state["4me"];
|
|
if (typeof extAn === "number" && extAn === wanted
|
|
&& extMe) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
_compute_aux_axis: function() {
|
|
// Auxiliary axis driven by the auxcnc ESP32 (typically
|
|
// exposed to gplan as A). Position, homed flag and
|
|
// presence come from the bbctrl AuxAxis driver via
|
|
// state.aux_*. No motor mapping, no soft-limit warnings
|
|
// on toolpath bounds (auxcnc enforces its own).
|
|
const enabled = !!this.state.aux_enabled;
|
|
const present = !!this.state.aux_present;
|
|
const homed = !!this.state.aux_homed;
|
|
const pos = this.state.aux_pos || 0;
|
|
|
|
let klass = `${homed ? "homed" : "unhomed"} axis-w`;
|
|
let state = present ? "UNHOMED" : "OFFLINE";
|
|
let icon = present ? "question-circle" : "plug";
|
|
let title = present
|
|
? "Click the home button to home the auxiliary axis."
|
|
: "Aux controller not connected on /dev/ttyUSB0.";
|
|
if (homed) {
|
|
state = "HOMED";
|
|
icon = "check-circle";
|
|
title = "Auxiliary axis successfully homed.";
|
|
} else if (!present) {
|
|
klass += " error";
|
|
}
|
|
|
|
return {
|
|
pos: pos,
|
|
abs: pos,
|
|
off: 0,
|
|
min: 0, max: 0, dim: 0,
|
|
pathMin: 0, pathMax: 0, pathDim: 0,
|
|
motor: -1,
|
|
enabled: enabled,
|
|
homingMode: "limit-switch",
|
|
homed: homed,
|
|
klass: klass,
|
|
state: state,
|
|
icon: icon,
|
|
title: title,
|
|
ticon: "check-circle",
|
|
tstate: "OK",
|
|
toolmsg: "Auxiliary axis is not constrained by tool path bounds.",
|
|
tklass: `${homed ? "homed" : "unhomed"} axis-w`,
|
|
isAux: true,
|
|
};
|
|
},
|
|
|
|
_compute_axes: function() {
|
|
let homed = false;
|
|
|
|
for (const name of "xyzabc") {
|
|
const axis = this[name];
|
|
|
|
if (!axis.enabled) {
|
|
continue;
|
|
}
|
|
|
|
if (!axis.homed) {
|
|
homed = false; break;
|
|
}
|
|
|
|
homed = true;
|
|
}
|
|
|
|
let error = false;
|
|
let warn = false;
|
|
|
|
if (homed) {
|
|
for (const name of "xyzabc") {
|
|
const axis = this[name];
|
|
|
|
if (!axis.enabled) {
|
|
continue;
|
|
}
|
|
|
|
if (axis.klass.indexOf("error") != -1) {
|
|
error = true;
|
|
}
|
|
|
|
if (axis.klass.indexOf("warn") != -1) {
|
|
warn = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
let klass = homed ? "homed" : "unhomed";
|
|
if (error) {
|
|
klass += " error";
|
|
} else if (warn) {
|
|
klass += " warn";
|
|
}
|
|
|
|
if (!homed && this.ask_home) {
|
|
this.ask_home = false;
|
|
SvelteComponents.showDialog("HomeMachine", {
|
|
home: () => this.home()
|
|
});
|
|
}
|
|
|
|
return {
|
|
homed: homed,
|
|
klass: klass
|
|
};
|
|
}
|
|
}
|
|
};
|