Mach.home: defer external A homing until AVR axes finish
Previously the homing loop iterated zxyabc and processed each axis
in turn, but the AVR axes (Z/X/Y) just queue G-code to the planner
and return immediately - the gantry keeps moving in the background.
The external A homing was then driven synchronously on the same
loop iteration, which meant the W stepper started its limit-seek
*while the gantry was still actively homing Z/X/Y*. Visually
confusing and unsafe.
Split into two phases:
1. The HTTP handler thread queues every AVR axis (no change) and
collects external axes into a deferred list.
2. A background thread polls cycle until it returns to 'idle'
(signalling the AVR finished its queued homing). It then runs
each external axis home in order, blocking on the ESP serial
RPC. Post-home bookkeeping (set_axis to AVR, planner G92, cycle
reset) is scheduled back onto the IOLoop via add_callback so
gplan and the AVR command queue are only touched from one
thread.
A guard prevents overlapping threads if Home is clicked again while
the previous deferred run is still waiting.
This commit is contained in:
@@ -215,8 +215,6 @@ 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
|
||||||
@@ -230,31 +228,24 @@ 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
|
||||||
|
|
||||||
@@ -270,20 +261,13 @@ 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'",
|
||||||
|
:disabled="!w.enabled",
|
||||||
title="Home W axis.", @click="aux_home()")
|
title="Home W axis.", @click="aux_home()")
|
||||||
.fa.fa-home
|
.fa.fa-home
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -357,41 +371,10 @@ class Mach(Comm):
|
|||||||
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
|
||||||
cycle_started = False
|
# in _run_external_homing below.
|
||||||
try:
|
external_pending.append((axis, ext))
|
||||||
self._begin_cycle('homing')
|
|
||||||
cycle_started = True
|
|
||||||
ext.home()
|
|
||||||
home_mm = ext.home_position_mm
|
|
||||||
# 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 as e:
|
|
||||||
self.mlog.error(
|
|
||||||
'External axis homing failed: %s' % e)
|
|
||||||
# Make sure we clean up the cycle so the UI does
|
|
||||||
# not stay locked out of jog/home/start. The AVR
|
|
||||||
# never moved during external homing, so its
|
|
||||||
# state never changes back to READY (which is
|
|
||||||
# what _update normally relies on to exit the
|
|
||||||
# cycle). Drop straight to idle.
|
|
||||||
if cycle_started and self._get_cycle() == 'homing':
|
|
||||||
try:
|
|
||||||
self._set_cycle('idle')
|
|
||||||
except Exception:
|
|
||||||
self.mlog.exception(
|
|
||||||
'Failed to reset cycle to idle '
|
|
||||||
'after external homing error')
|
|
||||||
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
|
||||||
@@ -424,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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user