mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-19 03:49:50 -05:00
Add optional Z-hop and in-dock retract to the return after a tool change
With restore_axis naming X or Y, a real tool change can now raise the old tool before it crosses the part to its dock, retract the new tool while it is still seated in its dock, travel back raised, descend, and give back part of the retract at the target. The options are restore_z_hop, restore_retract and restore_unretract with their feeds; all are zero as shipped. From Niels Sedat's #29 without the prime-tower registration, the model-skip rule, the shared-stepper sync or the stats hooks. Co-authored-by: Niels Sedat <nlssdt@gmail.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7e8ea9dbf4
commit
308d97ef21
5 files changed
+371
-6
No files matched your search
@@ -24,6 +24,14 @@ both printer models or through every real-world workflow.
|
||||
default; nothing changes for a profile that does not call them.
|
||||
- `printer.ff_print` reports the active plate's prime tower: position, size,
|
||||
brim, rotation and measured outline.
|
||||
- Gentler return to the print after a tool change, for a printer that restores
|
||||
X/Y (`restore_axis`): `restore_z_hop` raises the old tool before it crosses the
|
||||
part and the new one travels back high, `restore_retract` pulls filament back
|
||||
while the new tool is still seated in its dock so it does not string, and
|
||||
`restore_unretract` gives part of it back at the target. All are zero as
|
||||
shipped, which changes nothing. From
|
||||
[#29](https://github.com/Klipper4FlashForge/firmware/pull/29), without its
|
||||
prime-tower handling.
|
||||
- The shared-stepper adapter `ff_extruder.py`, for a printer whose four
|
||||
logical extruders repeat one physical stepper's options. It does nothing
|
||||
until a `[ff_extruder]` section is configured, and none is yet.
|
||||
|
||||
@@ -347,6 +347,23 @@ parking zeroes the tool offsets and the same G-code Z becomes a different
|
||||
machine Z (~3.2 mm, this tool's nozzle-to-station-trigger gap -- the station,
|
||||
not the eddy coil; see the note under Calibration).
|
||||
|
||||
When X or Y is restored after a real change, the trip back can be made gentler
|
||||
on the part. Three groups of options in `[ff_toolchange]`, all zero as shipped:
|
||||
|
||||
- `restore_z_hop` raises the old tool that far before it crosses the part to
|
||||
its dock, and the new tool travels back at that height and descends at the
|
||||
target.
|
||||
- `restore_retract` pulls that much filament back while the new tool is still
|
||||
seated in its dock, before it is drawn out, so a hot nozzle does not drag an
|
||||
ooze string. A tool below the minimum extrusion temperature is not retracted.
|
||||
- `restore_unretract` pushes back at most that much at the target. Setting it
|
||||
below the retract is deliberate: the slicer's own extrusion supplies the rest
|
||||
while moving, instead of a blob from a stationary prime.
|
||||
|
||||
Feeds are `restore_z_feed`, `restore_retract_feed` and
|
||||
`restore_unretract_feed`. Re-selecting the tool that is already mounted, and any
|
||||
change that does not restore X or Y, behaves as before.
|
||||
|
||||
`SET_TOOL_TEMPERATURE` addresses the extruder behind the tool; `WAIT=1` waits
|
||||
only for heat-up, as Klipper's own `TEMPERATURE_WAIT MINIMUM` does.
|
||||
`VERIFY_TOOL_DETECTED` accepts `ASYNC` and ignores it — we read switches after
|
||||
|
||||
@@ -125,6 +125,26 @@ accel_move: 8000
|
||||
#restore_axis: xy
|
||||
#restore_feed: 9000
|
||||
|
||||
# The trip across the part and back, used only when restore_axis names X or Y
|
||||
# and the tool really changes. Off as shipped; set them in your printer.cfg.
|
||||
# restore_z_hop mm. The old tool rises this far before it crosses the
|
||||
# part to its dock, and the new one travels back this
|
||||
# high, then descends at the target.
|
||||
# restore_retract mm of filament the new tool pulls back while it is still
|
||||
# seated in its dock, before it is drawn out, so a hot
|
||||
# nozzle does not string. Skipped below the minimum
|
||||
# extrusion temperature.
|
||||
# restore_unretract mm pushed back at the target, at most restore_retract.
|
||||
# Less than the retract on purpose: the slicer's own
|
||||
# extrusion supplies the rest while moving.
|
||||
# restore_z_feed, restore_retract_feed, restore_unretract_feed mm/min
|
||||
#restore_z_hop: 2.0
|
||||
#restore_z_feed: 1200
|
||||
#restore_retract: 0.4
|
||||
#restore_retract_feed: 1800
|
||||
#restore_unretract: 0.4
|
||||
#restore_unretract_feed: 200
|
||||
|
||||
# Sensor names -- microswitches, exposed as [gcode_button] sections in
|
||||
# printer.base.cfg, but what they ARE is the app's sensors. dock_sensors is
|
||||
# one per tool, PRESSED meaning that tool IS docked; grab_sensors are OR'd,
|
||||
|
||||
@@ -297,6 +297,29 @@ class FFToolchange:
|
||||
self.restore_axis = self._parse_axes(
|
||||
config.get('restore_axis', ''), config.error, 'restore_axis')
|
||||
self.restore_feed = config.getint('restore_feed', 9000, minval=1)
|
||||
# Protection for the trip back to the restored X/Y after a real
|
||||
# change, used only when restore_axis names X or Y. The old tool is
|
||||
# raised restore_z_hop before it crosses the part to its dock; the
|
||||
# new tool, still seated in its dock, retracts restore_retract of
|
||||
# filament before it is pulled out so a hot nozzle does not drag an
|
||||
# ooze string; and it travels at the raised Z, descends, then
|
||||
# unretracts restore_unretract at the target. Raising and retracting
|
||||
# are separate switches. All default to zero, which leaves the
|
||||
# change exactly as it was.
|
||||
self.restore_z_hop = config.getfloat('restore_z_hop', 0., minval=0.)
|
||||
self.restore_z_feed = config.getint('restore_z_feed', 1200, minval=1)
|
||||
self.restore_retract = config.getfloat(
|
||||
'restore_retract', 0., minval=0.)
|
||||
self.restore_retract_feed = config.getint(
|
||||
'restore_retract_feed', 1800, minval=1)
|
||||
# Less than the retract is deliberate: the slicer's own extrusion
|
||||
# fills the rest while moving, instead of a blob from a stationary
|
||||
# prime at the target.
|
||||
self.restore_unretract = config.getfloat(
|
||||
'restore_unretract', self.restore_retract, minval=0.,
|
||||
maxval=self.restore_retract)
|
||||
self.restore_unretract_feed = config.getint(
|
||||
'restore_unretract_feed', self.restore_retract_feed, minval=1)
|
||||
|
||||
# Sensor names.
|
||||
# position buttons: one per tool, PRESSED == that tool is docked.
|
||||
@@ -782,7 +805,55 @@ class FFToolchange:
|
||||
gcode_move = self.printer.lookup_object('gcode_move')
|
||||
return list(gcode_move.get_status()['gcode_position'])
|
||||
|
||||
def _restore_position(self, axes, pos):
|
||||
def _travel_protected(self, axes):
|
||||
"""Does a change that restores `axes` raise, retract and unretract?"""
|
||||
return (('X' in axes or 'Y' in axes)
|
||||
and (self.restore_z_hop > 0. or self.restore_retract > 0.))
|
||||
|
||||
def _retract_in_dock(self):
|
||||
"""Retract the newly locked tool before it leaves its dock.
|
||||
|
||||
The lock macro is synchronous (STEPPER_LOCK ends in M400), so the
|
||||
tool and its electrical contacts are seated when this is called.
|
||||
Relative E is borrowed for the move, and RESTORE_GCODE_STATE keeps
|
||||
the slicer's own E coordinate. Returns the distance retracted.
|
||||
"""
|
||||
if self.restore_retract <= 0.:
|
||||
return 0.
|
||||
extruder = self.printer.lookup_object('toolhead').get_extruder()
|
||||
if not extruder.get_status(
|
||||
self.reactor.monotonic())['can_extrude']:
|
||||
self.gcode.respond_info(
|
||||
"ff_toolchange: in-dock retract skipped because the"
|
||||
" mounted tool is below minimum extrusion temperature")
|
||||
return 0.
|
||||
self._run('SAVE_GCODE_STATE NAME=_ff_dock_retract')
|
||||
try:
|
||||
self._run('M83')
|
||||
self._run('G1 E-%.3f F%d'
|
||||
% (self.restore_retract, self.restore_retract_feed))
|
||||
finally:
|
||||
self._run('RESTORE_GCODE_STATE NAME=_ff_dock_retract')
|
||||
return self.restore_retract
|
||||
|
||||
def _raise_before_docking(self, pos):
|
||||
"""Raise the mounted nozzle before it crosses the print to its dock.
|
||||
|
||||
Z stays raised through the release and the grab; _restore_position
|
||||
lowers it once the new tool is back over the target X/Y.
|
||||
"""
|
||||
if self.restore_z_hop <= 0.:
|
||||
return
|
||||
self._run('SAVE_GCODE_STATE NAME=_ff_dock_zhop')
|
||||
try:
|
||||
self._run('G90')
|
||||
self._run('G1 Z%.3f F%d'
|
||||
% (pos[2] + self.restore_z_hop, self.restore_z_feed))
|
||||
finally:
|
||||
self._run('RESTORE_GCODE_STATE NAME=_ff_dock_zhop')
|
||||
|
||||
def _restore_position(self, axes, pos, protected=False,
|
||||
return_retract=0.):
|
||||
"""Put the toolhead back where the change found it.
|
||||
|
||||
A GCODE position is captured and replayed, not a machine one, so it
|
||||
@@ -791,7 +862,9 @@ class FFToolchange:
|
||||
the point of the parameter.
|
||||
|
||||
X/Y first and Z last: descending before the carriage is over the
|
||||
target would drag the nozzle across the part.
|
||||
target would drag the nozzle across the part. With `protected` the
|
||||
trip is made restore_z_hop higher, and the Z descent is followed by
|
||||
the unretract of the filament pulled back in the dock.
|
||||
|
||||
Restoring Z after a PARK is the one sharp edge. Parking zeroes the
|
||||
tool offsets, so the same G-code Z is a different machine Z -- by
|
||||
@@ -802,13 +875,27 @@ class FFToolchange:
|
||||
return
|
||||
xy_move = ' '.join('%s%.3f' % (letter, pos[i])
|
||||
for i, letter in enumerate('XY') if letter in axes)
|
||||
hop = protected and bool(xy_move) and self.restore_z_hop > 0.
|
||||
unretract = (min(return_retract, self.restore_unretract)
|
||||
if protected and xy_move else 0.)
|
||||
# The sequence has already put the modal state back; borrow it and
|
||||
# return it rather than leaving G90 and the restore feed behind.
|
||||
self._run('SAVE_GCODE_STATE NAME=_ff_restore_axis')
|
||||
try:
|
||||
self._run('G90')
|
||||
if hop:
|
||||
# The new tool's Z frame differs from the old one's, so the
|
||||
# hop is applied again here rather than assumed.
|
||||
self._run('G1 Z%.3f F%d'
|
||||
% (pos[2] + self.restore_z_hop, self.restore_z_feed))
|
||||
if xy_move:
|
||||
self._run('G1 %s F%d' % (xy_move, self.restore_feed))
|
||||
if hop:
|
||||
self._run('G1 Z%.3f F%d' % (pos[2], self.restore_z_feed))
|
||||
if unretract > 0.:
|
||||
self._run('M83')
|
||||
self._run('G1 E%.3f F%d'
|
||||
% (unretract, self.restore_unretract_feed))
|
||||
if 'Z' in axes:
|
||||
self._run('G1 Z%.3f F%d' % (pos[2], self.restore_feed))
|
||||
finally:
|
||||
@@ -862,9 +949,13 @@ class FFToolchange:
|
||||
|
||||
# ---------------- grab ----------------
|
||||
|
||||
def _grab(self, tool):
|
||||
"""Port of CommMgr::doGrabExtruderLatest @0x7a8190."""
|
||||
def _grab(self, tool, retract_in_dock=False):
|
||||
"""Port of CommMgr::doGrabExtruderLatest @0x7a8190.
|
||||
|
||||
Returns the filament retracted in the dock, which is owed back when
|
||||
the tool reaches its restored position."""
|
||||
dock_x, dock_y = self._dock(tool)
|
||||
return_retract = 0.
|
||||
|
||||
# Precheck: the target must be detected in its dock (20 x 50 ms).
|
||||
# No motion at all on failure.
|
||||
@@ -902,6 +993,13 @@ class FFToolchange:
|
||||
|
||||
if self._poll_until(self._grab_sensor, SEAT_TIMEOUT):
|
||||
self._run(self.grab_macro)
|
||||
if retract_in_dock:
|
||||
# Select this tool's heater and motion queue before
|
||||
# touching E, and retract before the pullback can
|
||||
# draw an ooze string out of the dock.
|
||||
self._run('ACTIVATE_EXTRUDER EXTRUDER=%s'
|
||||
% self._extruder_name(tool))
|
||||
return_retract = self._retract_in_dock()
|
||||
# The pullback feed is the app's literal F4800
|
||||
# (@0x7a9074), NOT the calibrated slow feed.
|
||||
self._run('G1 X%.3f F%d'
|
||||
@@ -948,6 +1046,7 @@ class FFToolchange:
|
||||
# sensors become the live ones (the app does this 3 s later
|
||||
# from a thread; here the grab moves are already complete).
|
||||
self._arm_runout(tool)
|
||||
return return_retract
|
||||
|
||||
# ---------------- release ----------------
|
||||
|
||||
@@ -1077,6 +1176,8 @@ class FFToolchange:
|
||||
# succeeded, since a half-finished sequence has no position worth
|
||||
# returning to.
|
||||
resume = self._capture_position() if restore_axis else None
|
||||
protected = self._travel_protected(restore_axis)
|
||||
return_retract = 0.
|
||||
self.changing = True
|
||||
try:
|
||||
self._wait_moves()
|
||||
@@ -1087,10 +1188,12 @@ class FFToolchange:
|
||||
current, _ = self._derive_current_tool()
|
||||
if current != tool:
|
||||
if current >= 0:
|
||||
if protected:
|
||||
self._raise_before_docking(resume)
|
||||
self._release(current)
|
||||
# _grab activates the extruder and applies the tool offsets,
|
||||
# as the app does inside doGrabExtruderLatest.
|
||||
self._grab(tool)
|
||||
return_retract = self._grab(tool, retract_in_dock=protected)
|
||||
else:
|
||||
# Same tool re-selected: still re-activate and re-apply, so
|
||||
# the first Tn after a RESTART (which wiped the gcode
|
||||
@@ -1105,7 +1208,12 @@ class FFToolchange:
|
||||
# channel. Upstream needs none: both apply to the ACTIVE extruder,
|
||||
# which _grab has just set to this tool.
|
||||
if resume is not None:
|
||||
self._restore_position(restore_axis, resume)
|
||||
# Re-selecting the mounted tool crossed nothing and retracted
|
||||
# nothing, so it keeps the plain X/Y-first return.
|
||||
self._restore_position(
|
||||
restore_axis, resume,
|
||||
protected=protected and current != tool,
|
||||
return_retract=return_retract)
|
||||
except FFToolchangeError as err:
|
||||
raise gcmd.error(str(err))
|
||||
except self.printer.command_error:
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
"""The trip across the part during a tool change: hop, in-dock retract, unretract.
|
||||
|
||||
These run the real _toolchange, _restore_position, _retract_in_dock and
|
||||
_raise_before_docking. The dock mechanics (_release, _grab's lock dance) are
|
||||
substituted, so they do not validate motion on a printer; they pin the order
|
||||
of the commands and which changes are left exactly as they were.
|
||||
"""
|
||||
import importlib.util
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from lib.paths import ROOT
|
||||
|
||||
pytestmark = pytest.mark.static
|
||||
|
||||
MODULE = (ROOT / "pkgs" / "klipper" / "payload" / "klipper" / "klippy" /
|
||||
"extras" / "ff_toolchange.py")
|
||||
POS = [100., 50., 3.2, 0.]
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Extruder:
|
||||
def __init__(self, can_extrude=True):
|
||||
self.can_extrude = can_extrude
|
||||
|
||||
def get_status(self, eventtime):
|
||||
return {"can_extrude": self.can_extrude}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def travel(monkeypatch):
|
||||
spec = importlib.util.spec_from_file_location("anvil_ff_toolchange", MODULE)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
tc = module.FFToolchange.__new__(module.FFToolchange)
|
||||
tc.log = []
|
||||
tc.changing = False
|
||||
tc.restore_axis = "xy"
|
||||
tc.restore_feed = 9000
|
||||
tc.restore_z_hop = 2.0
|
||||
tc.restore_z_feed = 1200
|
||||
tc.restore_retract = 0.4
|
||||
tc.restore_retract_feed = 1800
|
||||
tc.restore_unretract = 0.3
|
||||
tc.restore_unretract_feed = 200
|
||||
tc.extruder = Extruder()
|
||||
tc.mounted = 0
|
||||
tc.gcode = SimpleNamespace(respond_info=tc.log.append)
|
||||
tc.reactor = SimpleNamespace(monotonic=lambda: 0.)
|
||||
tc.printer = SimpleNamespace(
|
||||
command_error=CommandError,
|
||||
lookup_object=lambda name, default=None: SimpleNamespace(
|
||||
get_extruder=lambda: tc.extruder))
|
||||
tc.tools = [SimpleNamespace(
|
||||
extruder_name="extruder" if i == 0 else "extruder%d" % i)
|
||||
for i in range(4)]
|
||||
|
||||
monkeypatch.setattr(tc, "_run", lambda script: tc.log.append(script))
|
||||
monkeypatch.setattr(tc, "_wait_moves", lambda: None)
|
||||
monkeypatch.setattr(tc, "_ensure_homed", lambda *a: None)
|
||||
monkeypatch.setattr(tc, "_capture_position", lambda: list(POS))
|
||||
monkeypatch.setattr(tc, "_derive_current_tool",
|
||||
lambda: (tc.mounted, "sensors"))
|
||||
monkeypatch.setattr(tc, "_set_tool_frame", lambda tool: None)
|
||||
monkeypatch.setattr(tc, "_arm_runout", lambda tool: None)
|
||||
monkeypatch.setattr(tc, "_release",
|
||||
lambda tool: tc.log.append("release T%d" % tool))
|
||||
|
||||
def grab(tool, retract_in_dock=False):
|
||||
tc.log.append("grab T%d in_dock=%s" % (tool, retract_in_dock))
|
||||
return tc._retract_in_dock() if retract_in_dock else 0.
|
||||
|
||||
monkeypatch.setattr(tc, "_grab", grab)
|
||||
gcmd = SimpleNamespace(get=lambda name, default=None: default,
|
||||
error=CommandError)
|
||||
return tc, gcmd
|
||||
|
||||
|
||||
def change(tc, gcmd, tool=2):
|
||||
tc._toolchange(gcmd, tool)
|
||||
return tc.log
|
||||
|
||||
|
||||
def test_nothing_set_leaves_the_change_exactly_as_it_was(travel):
|
||||
tc, gcmd = travel
|
||||
tc.restore_z_hop = tc.restore_retract = 0.
|
||||
tc.restore_unretract = 0.
|
||||
|
||||
assert change(tc, gcmd) == [
|
||||
"release T0", "grab T2 in_dock=False",
|
||||
"SAVE_GCODE_STATE NAME=_ff_restore_axis", "G90",
|
||||
"G1 X100.000 Y50.000 F9000",
|
||||
"RESTORE_GCODE_STATE NAME=_ff_restore_axis"]
|
||||
|
||||
|
||||
def test_a_protected_change_raises_retracts_travels_lowers_and_unretracts(travel):
|
||||
tc, gcmd = travel
|
||||
|
||||
assert change(tc, gcmd) == [
|
||||
# the old tool rises before it crosses the part to its dock
|
||||
"SAVE_GCODE_STATE NAME=_ff_dock_zhop", "G90", "G1 Z5.200 F1200",
|
||||
"RESTORE_GCODE_STATE NAME=_ff_dock_zhop",
|
||||
"release T0",
|
||||
# the new tool pulls filament back while it is still seated
|
||||
"grab T2 in_dock=True",
|
||||
"SAVE_GCODE_STATE NAME=_ff_dock_retract", "M83", "G1 E-0.400 F1800",
|
||||
"RESTORE_GCODE_STATE NAME=_ff_dock_retract",
|
||||
# travel high, descend, give back part of it
|
||||
"SAVE_GCODE_STATE NAME=_ff_restore_axis", "G90",
|
||||
"G1 Z5.200 F1200", "G1 X100.000 Y50.000 F9000", "G1 Z3.200 F1200",
|
||||
"M83", "G1 E0.300 F200",
|
||||
"RESTORE_GCODE_STATE NAME=_ff_restore_axis"]
|
||||
|
||||
|
||||
def test_the_unretract_never_exceeds_what_was_retracted(travel, monkeypatch):
|
||||
tc, gcmd = travel
|
||||
tc.restore_unretract = 0.4
|
||||
monkeypatch.setattr(tc, "_retract_in_dock", lambda: 0.1)
|
||||
|
||||
log = change(tc, gcmd)
|
||||
|
||||
assert "G1 E0.100 F200" in log
|
||||
|
||||
|
||||
def test_a_hop_alone_does_not_touch_the_filament(travel):
|
||||
tc, gcmd = travel
|
||||
tc.restore_retract = tc.restore_unretract = 0.
|
||||
|
||||
log = change(tc, gcmd)
|
||||
|
||||
assert "G1 Z5.200 F1200" in log and "G1 Z3.200 F1200" in log
|
||||
assert not any(command.startswith("G1 E") for command in log)
|
||||
|
||||
|
||||
def test_a_retract_alone_does_not_move_z(travel):
|
||||
tc, gcmd = travel
|
||||
tc.restore_z_hop = 0.
|
||||
|
||||
log = change(tc, gcmd)
|
||||
|
||||
assert "G1 E-0.400 F1800" in log and "G1 E0.300 F200" in log
|
||||
assert not any(command.startswith("G1 Z") for command in log)
|
||||
|
||||
|
||||
def test_a_cold_tool_is_not_retracted_or_unretracted(travel):
|
||||
tc, gcmd = travel
|
||||
tc.extruder = Extruder(can_extrude=False)
|
||||
|
||||
log = change(tc, gcmd)
|
||||
|
||||
assert not any(command.startswith("G1 E") for command in log)
|
||||
assert any("below minimum extrusion temperature" in line
|
||||
for line in log if not line.startswith(("G", "M", "SAVE")))
|
||||
|
||||
|
||||
def test_the_first_pickup_has_nothing_to_raise_but_still_retracts(travel):
|
||||
tc, gcmd = travel
|
||||
tc.mounted = -1
|
||||
|
||||
log = change(tc, gcmd)
|
||||
|
||||
assert "SAVE_GCODE_STATE NAME=_ff_dock_zhop" not in log
|
||||
assert "G1 E-0.400 F1800" in log and "G1 E0.300 F200" in log
|
||||
|
||||
|
||||
def test_reselecting_the_mounted_tool_keeps_the_plain_return(travel):
|
||||
tc, gcmd = travel
|
||||
tc.mounted = 2
|
||||
|
||||
log = change(tc, gcmd, tool=2)
|
||||
|
||||
assert "release T2" not in log
|
||||
assert log[-4:] == ["SAVE_GCODE_STATE NAME=_ff_restore_axis", "G90",
|
||||
"G1 X100.000 Y50.000 F9000",
|
||||
"RESTORE_GCODE_STATE NAME=_ff_restore_axis"]
|
||||
assert not any(command.startswith("G1 E") for command in log)
|
||||
assert not any(command.startswith("G1 Z") for command in log)
|
||||
|
||||
|
||||
def test_without_xy_restored_the_protection_is_off(travel):
|
||||
tc, gcmd = travel
|
||||
tc.restore_axis = ""
|
||||
|
||||
log = change(tc, gcmd)
|
||||
|
||||
assert log == ["release T0", "grab T2 in_dock=False"]
|
||||
|
||||
|
||||
def test_z_only_restore_is_not_protected(travel):
|
||||
tc, gcmd = travel
|
||||
tc.restore_axis = "Z"
|
||||
|
||||
log = change(tc, gcmd)
|
||||
|
||||
assert "grab T2 in_dock=False" in log
|
||||
assert "SAVE_GCODE_STATE NAME=_ff_dock_zhop" not in log
|
||||
assert not any(command.startswith("G1 E") for command in log)
|
||||
|
||||
|
||||
def test_the_hop_follows_the_captured_z_not_a_fixed_height(travel, monkeypatch):
|
||||
tc, gcmd = travel
|
||||
monkeypatch.setattr(tc, "_capture_position",
|
||||
lambda: [10., 20., 0.6, 0.])
|
||||
|
||||
log = change(tc, gcmd)
|
||||
|
||||
assert log.count("G1 Z2.600 F1200") == 2
|
||||
assert "G1 Z0.600 F1200" in log
|
||||
Reference in new issue
Block a user