Files
onefinity-firmware/src/js/axis-vars.js
Henrik Muehe 6dbc7e6d04 ExternalAxis: virtual A axis through gplan, mirrored on the ESP
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.
2026-05-03 14:17:46 +02:00

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
};
}
}
};