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.
This commit is contained in:
2026-05-03 14:17:46 +02:00
parent 32bf65934b
commit 6d4c51bd49
6 changed files with 693 additions and 12 deletions

View File

@@ -107,8 +107,14 @@ class State(object):
def reset(self):
# Unhome all motors
for i in range(4): self.set('%dhomed' % i, False)
# Unhome all motors (real AVR motors 0..3 and the synthetic
# 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
for axis in 'xyzabc':
@@ -280,8 +286,11 @@ class State(object):
axis_motors = {axis: self.find_motor(axis) for axis in 'xyzabc'}
axis_vars = {}
# NOTE: motor index '4' is a host-only synthetic motor used
# by ExternalAxis to expose the auxcnc ESP-driven stepper as
# an additional axis. Real AVR motors are 0..3.
for name, value in vars.items():
if name[0] in '0123':
if name[0] in '01234':
motor = int(name[0])
for axis in 'xyzabc':
@@ -330,6 +339,9 @@ class State(object):
def get_axis_vector(self, name, scale = 1):
v = {}
# 0..3 are AVR motor channels. 4 is the host-side synthetic
# motor used by ExternalAxis. find_motor returns the right
# index regardless of whether the axis is physical or external.
for axis in 'xyzabc':
motor = self.find_motor(axis)
@@ -351,7 +363,10 @@ class State(object):
def find_motor(self, axis):
for motor in range(4):
# Walk 0..4: 0..3 are real AVR motors, 4 is the synthetic
# host-side motor used to expose the auxcnc ESP stepper as
# an external axis.
for motor in range(5):
if not ('%dan' % motor) in self.vars: continue
motor_axis = 'xyzabc'[self.vars['%dan' % motor]]
if motor_axis == axis.lower() and self.vars.get('%dme' % motor, 0):