Compare commits
3 Commits
7cdab010b3
...
3b622d3d17
| Author | SHA1 | Date | |
|---|---|---|---|
| 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/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -140,6 +140,51 @@ 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_w', 0.0))
|
||||||
|
hi = float(self.aux._cfg.get('max_w', 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))
|
||||||
|
|
||||||
# ----------------------------------------------------------- conversion
|
# ----------------------------------------------------------- conversion
|
||||||
|
|
||||||
def mm_to_steps_delta(self, delta_mm):
|
def mm_to_steps_delta(self, delta_mm):
|
||||||
@@ -265,12 +310,20 @@ 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)
|
||||||
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,29 +339,35 @@ 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
|
"""Non-blocking variant: post a target to the worker queue.
|
||||||
and update the host's notion of the axis position immediately
|
Used by Planner.__encode in the hot path.
|
||||||
so subsequent line splits compute correct deltas.
|
|
||||||
|
We deliberately do NOT mirror the new target into state.<axis>p
|
||||||
|
here. The AVR also receives the line block (we don't strip
|
||||||
|
the external axis from the AVR target) and its trapezoid
|
||||||
|
progressively updates ex.position[A] -> reports back as ap.
|
||||||
|
If we set state.ap here ahead of the AVR, the DRO would jump
|
||||||
|
to the target and then snap back to intermediate values as
|
||||||
|
AVR reports stream in. Instead we let the AVR drive state.ap.
|
||||||
|
We still mirror the new abs into our internal _pos_mm so
|
||||||
|
the next line block computes the correct delta.
|
||||||
|
|
||||||
The Planner.__encode hook calls this so the AVR comm thread
|
The Planner.__encode hook calls this so the AVR comm thread
|
||||||
is never blocked by serial RPCs to the ESP. v1 accepts that
|
is never blocked by serial RPCs to the ESP.
|
||||||
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
|
Soft limits are enforced here (defense in depth on top of
|
||||||
applied to both, so velocities and accelerations are bounded
|
gplan). Raising stops the planner via Planner.next's
|
||||||
by whichever axis is most constrained."""
|
exception handler."""
|
||||||
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)
|
||||||
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 _compute_move(self, ext_mm):
|
def _compute_move(self, ext_mm):
|
||||||
|
|||||||
@@ -334,6 +334,24 @@ 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():
|
||||||
@@ -341,16 +359,20 @@ class Mach(Comm):
|
|||||||
continue
|
continue
|
||||||
self.mlog.info('Homing external %s axis via auxcnc' %
|
self.mlog.info('Homing external %s axis via auxcnc' %
|
||||||
axis.upper())
|
axis.upper())
|
||||||
# ext.home() blocks on the ESP. Use the lower-level
|
|
||||||
# planner.mdi (not Mach.mdi) so we don't try to
|
|
||||||
# _begin_cycle('mdi') from inside the home-all loop
|
|
||||||
# which is already in the 'homing' cycle.
|
|
||||||
try:
|
try:
|
||||||
self._begin_cycle('homing')
|
self._begin_cycle('homing')
|
||||||
ext.home()
|
ext.home()
|
||||||
self.planner.mdi(
|
home_mm = ext.home_position_mm
|
||||||
'G28.3 %c%f' % (axis, ext.home_position_mm),
|
# 1) Update AVR: no motor steps, just position sync.
|
||||||
False)
|
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()
|
super().resume()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.mlog.error(
|
self.mlog.error(
|
||||||
|
|||||||
@@ -272,17 +272,11 @@ class Planner():
|
|||||||
if type == 'line':
|
if type == 'line':
|
||||||
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 +388,39 @@ 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.
|
|
||||||
|
We do NOT strip the external axis target from the AVR line.
|
||||||
|
The AVR's exec_move_to_target updates ex.position[axis] for
|
||||||
|
every axis in the target dict regardless of motor mapping,
|
||||||
|
and reports it back via the `p` indexed var. Leaving A in
|
||||||
|
the target keeps state.ap in sync with gplan's idea of A
|
||||||
|
(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.
|
||||||
|
|
||||||
For mixed XYZ + external moves the AVR runs XYZ at the
|
For mixed XYZ + external moves the AVR runs XYZ at the
|
||||||
gplan-computed rate while the ESP runs the external delta in
|
gplan-computed rate while the ESP runs the external delta
|
||||||
parallel. Pure external moves return None so __encode emits
|
in parallel. Final positions agree; intermediate ap reports
|
||||||
only the id-sync to keep planner ids advancing."""
|
from the AVR may briefly disagree with state.ap until the
|
||||||
target = dict(block['target'])
|
block completes."""
|
||||||
new_target, ext_mm = ext.split_target(target)
|
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_target_mm(ext_mm)
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user