mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-19 03:49:50 -05:00
Merge pull request #13 from Klipper4FlashForge/worktree-pa-sweep-chute
The PA sweep never moves the carriage, so park it over the purge chute
This commit is contained in:
3 files changed
+358
-52
No files matched your search
@@ -19,10 +19,16 @@
|
||||
# it gives the stock app.
|
||||
#
|
||||
# Four facts that shape this file:
|
||||
# * The lines are extruded ~8 mm above the plate, IN FREE AIR. Nothing
|
||||
# prints them and nothing ever looks at them -- they exist only to make a
|
||||
# pressure transient at the two 10x flow steps. Expect about a gram of
|
||||
# loose filament on the plate, and clear it before the next print.
|
||||
# * NOTHING IS DRAWN AND THE CARRIAGE DOES NOT MOVE. The sweep latches
|
||||
# enable_pin_tmc_x/y, which disables the X and Y drivers outright, so
|
||||
# every G1 X in the ladder is planned by Klipper and executed by nobody.
|
||||
# The ~1 g of filament falls straight down from wherever the toolhead was
|
||||
# parked. We park it over the PURGE CHUTE first, which is what the app
|
||||
# does (clearNozzleEddy builds X=275+tool_dx, Y=254+tool_dy from the same
|
||||
# constants ff-filament.cfg carries as purge_x/purge_y), so the extrudate
|
||||
# goes down the chute instead of onto the plate. The X/Y ladder is a
|
||||
# timing generator: it stretches each candidate across the two 10x flow
|
||||
# steps, and that is its whole job.
|
||||
# * Only the verdict 9 passes. Within a sweep the SMALLEST passing candidate
|
||||
# wins; the result is the mean of three sweep winners. So the answer can
|
||||
# never come out below the smallest candidate (0.0100 by default).
|
||||
@@ -42,14 +48,19 @@
|
||||
# dressed up as a measurement. See require_discrimination.
|
||||
# * cold extruder, empty tool and out-of-range geometry are refused before
|
||||
# anything moves, instead of failing mid-line with the eBoard armed.
|
||||
# * the app's SET_KINEMATIC_POSITION replay is NOT ported: our sweep is
|
||||
# plain G1 under Klipper's own kinematics, so there is nothing to correct
|
||||
# and replaying it would only lie to Klipper about where the carriage is.
|
||||
# * the driver latch is always released. It is put back to what it read on
|
||||
# entry, EXCEPT when that was itself the latched value -- a latch left on
|
||||
# by an earlier run is cleared rather than faithfully reinstated, which is
|
||||
# what would otherwise make the condition permanent. A failure to release
|
||||
# it is reported loudly instead of swallowed. (A klippy or MCU shutdown
|
||||
# needs none of this: [output_pin]'s shutdown_value defaults to 0, so the
|
||||
# MCU drives these pins low on its own.)
|
||||
# * the timing is NOT "improved". pa_action is an immediate MCU command and
|
||||
# there is deliberately no M400 between the last G1 and ACTION=0. The
|
||||
# closed scorer was tuned against that. Do not add barriers.
|
||||
#
|
||||
# FF_PA_CALIBRATE [TOOL=] [TEMP=] [SWEEPS=] [CANDIDATES=] [DRY_RUN=1]
|
||||
# FF_PA_CALIBRATE [TOOL=] [TEMP=] [SWEEPS=] [CANDIDATES=] [PARK=0]
|
||||
# [DRY_RUN=1]
|
||||
# the full sweep on the MOUNTED tool. Roughly 4-5 minutes and ~1 g of
|
||||
# filament for the full five sweeps. DRY_RUN=1 prints the whole script
|
||||
# and the computed Z without moving. SWEEPS=1 is one sweep, ~1 min.
|
||||
@@ -105,6 +116,23 @@
|
||||
# The X/Y driver latch the app holds for the duration, restored to whatever
|
||||
# it was. Empty disables the latch entirely.
|
||||
#tmc_pins: enable_pin_tmc_x, enable_pin_tmc_y
|
||||
# What "latched" and "released" mean on those pins. The app writes 1.00 to
|
||||
# arm and hardcodes 0.00 to release; a pin found sitting at tmc_latch_value
|
||||
# when a run starts is treated as debris and released on exit, not preserved.
|
||||
#tmc_latch_value: 1.0
|
||||
#tmc_released_value: 0.0
|
||||
# Park over the purge chute before the drivers are latched, and replay the
|
||||
# parked coordinates through SET_KINEMATIC_POSITION afterwards -- Klipper
|
||||
# spends the sweep believing the carriage walked the ladder, and it never
|
||||
# moved. park: False latches wherever the nozzle already stands and dumps the
|
||||
# gram THERE, which is only ever right if you have put something under it
|
||||
# yourself. The chute follows [gcode_macro _FF_FILAMENT]'s purge_x/purge_y
|
||||
# and the tool's own dx/dy; park_x/park_y override that outright.
|
||||
#park: True
|
||||
#park_x:
|
||||
#park_y:
|
||||
# The staging X the app approaches the chute from (its "G1 X250").
|
||||
#park_approach_dx: 25
|
||||
# pa_action's two arguments. The firmware tests == 11 for start and treats
|
||||
# everything else as stop; pc is stored and never read back on this eBoard
|
||||
# firmware, so 666 is cargo we keep for fidelity.
|
||||
|
||||
@@ -15,10 +15,28 @@
|
||||
# firmware unchanged, so we get the same verdicts the touchscreen does.
|
||||
# [pa_adjust] is the 40-line forwarder that carries the two commands.
|
||||
#
|
||||
# * THE LINES ARE EXTRUDED IN FREE AIR, ~8 mm above the bed, and nothing
|
||||
# ever looks at them. They exist only to make a pressure transient at the
|
||||
# two 10x flow steps. There is no first layer here and no adhesion to
|
||||
# get right -- just about a gram of loose filament on the plate.
|
||||
# * THE CARRIAGE DOES NOT MOVE, AND THAT IS THE WHOLE DESIGN. The prologue
|
||||
# latches enable_pin_tmc_x/y, which hard-disables the X and Y drivers, so
|
||||
# every `G1 X` in the sweep is planned by Klipper and executed by nobody.
|
||||
# The toolhead stays exactly where the prologue parked it and the ~1 g of
|
||||
# filament falls straight down from there. The app parks over the PURGE
|
||||
# CHUTE first -- clearNozzleEddy @0x78f030 builds the restore string from
|
||||
# X = 275 + tool_dx, Y = 254 + tool_dy, the same two constants
|
||||
# ff-filament.cfg carries as purge_x/purge_y -- so the extrudate goes down
|
||||
# the chute and never touches the plate. We now do the same.
|
||||
#
|
||||
# The X/Y ladder is therefore a TIMING GENERATOR and nothing else. It
|
||||
# exists only to stretch each candidate over the two 10x flow steps where
|
||||
# pressure advance can show up. Nothing draws lines, nothing looks at
|
||||
# them, and the scrambled candidate order buys no spatial separation
|
||||
# because there is no space -- it is kept because it is the app's.
|
||||
#
|
||||
# * SO SET_KINEMATIC_POSITION IS MANDATORY ON EXIT. After the sweep Klipper
|
||||
# believes the carriage walked to the end of the ladder while it never
|
||||
# left the chute. The app replays the parked coordinates (resetTmcPa
|
||||
# @0x79350c takes them as its argument) and so must we -- without it the
|
||||
# next move is planned from a position that is a lie, and it is homed
|
||||
# state that makes it dangerous rather than merely wrong.
|
||||
#
|
||||
# * THE MISSING M400 IS DELIBERATE. pa_action is an immediate MCU command,
|
||||
# so ACTION=0 fires while motion is still queued; what bounds it is
|
||||
@@ -69,7 +87,20 @@ ACTION_STOP = 0 # ... and treats everything else as stop
|
||||
ACTION_PC = 666 # the app's "material" code; inert on this eBoard
|
||||
LINE_Z = 8.0 # RAW eddy frame, == _FF_FILAMENT's purge_z
|
||||
LINE_Z_FEED = 3000
|
||||
# The purge chute, and the staging X the app approaches it from. Defaults
|
||||
# only: when [gcode_macro _FF_FILAMENT] is present its purge_x/purge_y and
|
||||
# per-tool dx/dy win, so the chute has ONE definition in the tree.
|
||||
PARK_X = 275.0 # @0xdb6b2c, == _FF_FILAMENT's purge_x
|
||||
PARK_Y = 254.0 # @0xdb6b28, == _FF_FILAMENT's purge_y
|
||||
PARK_APPROACH_DX = 25.0 # the app's "G1 X250" staging move
|
||||
PARK_X_FEED = 6000
|
||||
PARK_Y_FEED = 24000
|
||||
PARK_APPROACH_FEED = 6000
|
||||
TMC_PINS = ('enable_pin_tmc_x', 'enable_pin_tmc_y')
|
||||
# The latch, and the value that is NOT the latch. The app writes 1.00 to arm
|
||||
# and hardcodes 0.00 to release; printer.base.cfg ships these pins at 0.
|
||||
TMC_LATCH_VALUE = 1.0
|
||||
TMC_RELEASED_VALUE = 0.0
|
||||
FILAMENT_AREA_175 = 2.405 # mm^2, for the cross-section warning
|
||||
|
||||
|
||||
@@ -140,9 +171,21 @@ class FFPA:
|
||||
# definition of "8 mm above the plate" in the tree.
|
||||
self.line_z = config.getfloat('line_z', None)
|
||||
self.line_z_feed = config.getint('line_z_feed', LINE_Z_FEED, minval=1)
|
||||
# Park over the purge chute before the drivers are latched. The whole
|
||||
# ~1 g lands wherever this leaves the nozzle, so 'False' means "I have
|
||||
# put something under the nozzle myself" -- not "park somewhere else".
|
||||
self.park = config.getboolean('park', True)
|
||||
self.park_x = config.getfloat('park_x', None)
|
||||
self.park_y = config.getfloat('park_y', None)
|
||||
self.park_approach_dx = config.getfloat('park_approach_dx',
|
||||
PARK_APPROACH_DX)
|
||||
self.tmc_pin_names = [n.strip() for n
|
||||
in config.getlist('tmc_pins', list(TMC_PINS))
|
||||
if n.strip()]
|
||||
self.tmc_latch_value = config.getfloat('tmc_latch_value',
|
||||
TMC_LATCH_VALUE)
|
||||
self.tmc_released_value = config.getfloat('tmc_released_value',
|
||||
TMC_RELEASED_VALUE)
|
||||
self.action_start = config.getint('action_start', ACTION_START)
|
||||
self.action_stop = config.getint('action_stop', ACTION_STOP)
|
||||
self.action_pc = config.getint('action_pc', ACTION_PC)
|
||||
@@ -159,6 +202,9 @@ class FFPA:
|
||||
self.dry_run = False
|
||||
self.running = False
|
||||
self.last = None
|
||||
# Where the prologue left the nozzle, in G-code coordinates. What
|
||||
# _restore feeds SET_KINEMATIC_POSITION; None until a run parks.
|
||||
self.parked_at = None
|
||||
|
||||
self.gcode.register_command('FF_PA_CALIBRATE',
|
||||
self.cmd_FF_PA_CALIBRATE,
|
||||
@@ -316,7 +362,7 @@ class FFPA:
|
||||
" require_filament_sensor: False if the switch is wrong."
|
||||
% (self.name, tool))
|
||||
|
||||
def _check_bounds(self, gcmd):
|
||||
def _check_bounds(self, gcmd, tool):
|
||||
"""The ladder must fit the machine. The defaults sit well inside a
|
||||
Creator 5's envelope; an overridden ladder need not."""
|
||||
_toolhead, status = self._toolhead_status()
|
||||
@@ -326,6 +372,13 @@ class FFPA:
|
||||
xs = [self.x_start] + [seg[0] for seg in self.segments]
|
||||
ys = [self.y_start + self.y_step * i
|
||||
for i in range(len(self.candidates))]
|
||||
# The ladder is phantom motion, but Klipper still plans it and still
|
||||
# range-checks it. The park is the one move that is real, so it has
|
||||
# to fit for a different and more literal reason.
|
||||
if self.park:
|
||||
px, py, _xf, _yf, _af = self._chute(tool)
|
||||
xs += [px, px - self.park_approach_dx]
|
||||
ys.append(py)
|
||||
for axis, values, lo, hi in (('X', xs, low.x, high.x),
|
||||
('Y', ys, low.y, high.y)):
|
||||
if min(values) < lo or max(values) > hi:
|
||||
@@ -335,6 +388,20 @@ class FFPA:
|
||||
" y_start/y_step."
|
||||
% (self.name, axis, min(values), max(values), lo, hi))
|
||||
|
||||
def _where_note(self, tool, raw_z, z_target):
|
||||
"""Say plainly where the gram of filament is going to end up."""
|
||||
if not self.park:
|
||||
return (" ! PARK=0: the drivers are latched where the nozzle"
|
||||
" already stands, so the whole ~1 g lands THERE, at Z"
|
||||
" %.3f raw / %.3f gcode. Nothing will move it aside."
|
||||
% (raw_z, z_target))
|
||||
x, y, _xf, _yf, _af = self._chute(tool)
|
||||
return (" the carriage parks over the purge chute at X%.1f Y%.1f,"
|
||||
" Z %.3f raw / %.3f gcode, and STAYS THERE: the X/Y drivers"
|
||||
" are latched off for the sweep, so the ladder is timing"
|
||||
" only and the ~1 g goes down the chute."
|
||||
% (x, y, raw_z, z_target))
|
||||
|
||||
def _cross_section_note(self, extruder):
|
||||
"""Klipper caps extrude cross-section at 4*nozzle^2. The sweep's
|
||||
0.137 mm^2 is comfortable on a 0.4 nozzle and tight on a 0.2."""
|
||||
@@ -385,9 +452,60 @@ class FFPA:
|
||||
|
||||
# ---------------- the sweep ----------------
|
||||
|
||||
def _prologue(self, z_target):
|
||||
def _chute(self, tool):
|
||||
"""The purge chute in G-code coordinates for TOOL.
|
||||
|
||||
Follows [gcode_macro _FF_FILAMENT] when it is loaded so the chute has
|
||||
one definition in the tree, and falls back to the app's own constants
|
||||
when it is not. park_x/park_y in [ff_pa] override both."""
|
||||
ff = self.printer.lookup_object('gcode_macro _FF_FILAMENT', None)
|
||||
x, y = PARK_X, PARK_Y
|
||||
x_feed, y_feed = PARK_X_FEED, PARK_Y_FEED
|
||||
approach_feed = PARK_APPROACH_FEED
|
||||
if ff is not None:
|
||||
status = ff.get_status(self.reactor.monotonic())
|
||||
x = status.get('purge_x', x)
|
||||
y = status.get('purge_y', y)
|
||||
x_feed = status.get('travel_x_feed', x_feed)
|
||||
y_feed = status.get('travel_y_feed', y_feed)
|
||||
approach_feed = status.get('approach_feed', approach_feed)
|
||||
dx = status.get('tool_dx') or []
|
||||
dy = status.get('tool_dy') or []
|
||||
if tool < len(dx):
|
||||
x += dx[tool]
|
||||
if tool < len(dy):
|
||||
y += dy[tool]
|
||||
if self.park_x is not None:
|
||||
x = self.park_x
|
||||
if self.park_y is not None:
|
||||
y = self.park_y
|
||||
return x, y, int(x_feed), int(y_feed), int(approach_feed)
|
||||
|
||||
def _park(self, tool):
|
||||
"""Put the nozzle over the chute, in the app's own three moves.
|
||||
|
||||
This is the ONLY real XY motion in the whole procedure, and it has to
|
||||
happen while the drivers still answer -- the latch that follows is
|
||||
what makes the sweep itself go nowhere. Returns the parked position
|
||||
for SET_KINEMATIC_POSITION to replay, or None when parking is off."""
|
||||
if not self.park:
|
||||
return None
|
||||
x, y, x_feed, y_feed, approach_feed = self._chute(tool)
|
||||
# Same order as _FF_FILAMENT_PREP: stage clear of the chute on X,
|
||||
# square up on Y, then come in on X at the slow approach feed.
|
||||
self._run('G1 X%.3f F%d' % (x - self.park_approach_dx, x_feed))
|
||||
self._run('G1 Y%.3f F%d' % (y, y_feed))
|
||||
self._run('G1 X%.3f F%d' % (x, approach_feed))
|
||||
self._wait_moves()
|
||||
return x, y
|
||||
|
||||
def _prologue(self, tool, z_target):
|
||||
"""The app's prologue, plus SAVE_GCODE_STATE and an explicit G90 --
|
||||
it leaks its modal state and we do not."""
|
||||
it leaks its modal state and we do not.
|
||||
|
||||
ORDER MATTERS. Every real move -- the Z raise and the travel to the
|
||||
chute -- happens BEFORE the driver latch. Once the latch is on,
|
||||
nothing the toolhead is told to do reaches the machine."""
|
||||
self._run('SAVE_GCODE_STATE NAME=_ff_pa')
|
||||
self._run('G90')
|
||||
self._run('G92 E0')
|
||||
@@ -395,15 +513,20 @@ class FFPA:
|
||||
self._run('SET_VELOCITY_LIMIT ACCEL=%.0f' % self.sweep_accel)
|
||||
self._run('SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=%.3f'
|
||||
% self.sweep_scv)
|
||||
for pin_name, _pin in self.tmc_pins:
|
||||
self._run('SET_PIN PIN=%s VALUE=1.00' % pin_name)
|
||||
self._wait_moves()
|
||||
# Raise before travelling, never lower -- same rule as the purge in
|
||||
# ff-filament.cfg. The XY travel is the first line's own G1.
|
||||
# ff-filament.cfg.
|
||||
_toolhead, status = self._toolhead_status()
|
||||
if self.dry_run or status['position'].z < z_target:
|
||||
self._run('G1 Z%.3f F%d' % (z_target, self.line_z_feed))
|
||||
self._wait_moves()
|
||||
parked = self._park(tool)
|
||||
if parked is not None:
|
||||
self.parked_at = (parked[0], parked[1], z_target)
|
||||
# The latch goes on last, and comes off first in _restore.
|
||||
for pin_name, _pin in self.tmc_pins:
|
||||
self._run('SET_PIN PIN=%s VALUE=%.2f'
|
||||
% (pin_name, self.tmc_latch_value))
|
||||
self._wait_moves()
|
||||
|
||||
def _one_line(self, index, pa_text, extruder_name, verbose):
|
||||
"""One candidate: arm, set PA, draw the line, disarm, read a verdict.
|
||||
@@ -496,6 +619,22 @@ class FFPA:
|
||||
|
||||
# ---------------- state ----------------
|
||||
|
||||
def _pin_restore_value(self, pin, now):
|
||||
"""What this driver pin should read when the run is over.
|
||||
|
||||
Normally whatever it read on entry -- we put back what we found. The
|
||||
exception is finding it ALREADY LATCHED, which is not a state worth
|
||||
preserving: it means an earlier run died without releasing it, and
|
||||
faithfully restoring it would relatch the drivers on every run and
|
||||
make the condition permanent. A latch we did not set is still a latch
|
||||
we can clear, so clear it."""
|
||||
if pin is None:
|
||||
return self.tmc_released_value
|
||||
value = pin.get_status(now)['value']
|
||||
if abs(value - self.tmc_latch_value) < 1e-6:
|
||||
return self.tmc_released_value
|
||||
return value
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _guarded(self, extruder):
|
||||
"""Snapshot on entry, restore unconditionally on exit, every step
|
||||
@@ -511,7 +650,7 @@ class FFPA:
|
||||
'pa': ext.get('pressure_advance', 0.),
|
||||
'smooth': ext.get('smooth_time', 0.),
|
||||
'name': extruder.get_name(),
|
||||
'pins': [(n, (p.get_status(now)['value'] if p is not None else 0.))
|
||||
'pins': [(n, self._pin_restore_value(p, now))
|
||||
for n, p in self.tmc_pins],
|
||||
}
|
||||
try:
|
||||
@@ -529,12 +668,38 @@ class FFPA:
|
||||
action_cmd.send([self.action_stop, self.action_pc])
|
||||
except (FFPAError, self.printer.command_error):
|
||||
pass
|
||||
# 2. Driver pins back to what they were, not to the app's hardcoded
|
||||
# 0.00. Left latched, whatever they gate stays energised.
|
||||
# 2. Driver pins back to what they were (see _pin_restore_value),
|
||||
# not to the app's hardcoded 0.00. THIS is the step that gives X
|
||||
# and Y back: while the latch is on the carriage is dead, and
|
||||
# nothing later in a print will lift it -- Klipper's own stepper
|
||||
# enable is a different pin and knows nothing about this one.
|
||||
#
|
||||
# A klippy or MCU shutdown is the one case that heals itself:
|
||||
# [output_pin] hands shutdown_value (default 0) to
|
||||
# setup_start_value, so the MCU drives these pins low on its own
|
||||
# without needing us. Everything else -- a command_error, a
|
||||
# cancel, an exception out of the sweep -- comes back through
|
||||
# here, which is why a failure is reported rather than swallowed.
|
||||
stuck = []
|
||||
for pin_name, value in snapshot['pins']:
|
||||
try:
|
||||
self._run('SET_PIN PIN=%s VALUE=%.2f' % (pin_name, value))
|
||||
except self.printer.command_error:
|
||||
stuck.append(pin_name)
|
||||
if stuck:
|
||||
try:
|
||||
self.gcode.respond_info(
|
||||
"%s: !! COULD NOT UNLATCH %s. The X/Y drivers are"
|
||||
" disabled and nothing else will lift them -- the"
|
||||
" carriage will not move and homing will not work."
|
||||
" Recover with %s. (A klippy or MCU shutdown would"
|
||||
" clear them by itself via shutdown_value; this was"
|
||||
" not one.)"
|
||||
% (self.name, ", ".join(stuck),
|
||||
" ".join("SET_PIN PIN=%s VALUE=%.2f"
|
||||
% (n, self.tmc_released_value)
|
||||
for n in stuck)))
|
||||
except Exception:
|
||||
pass
|
||||
# 3. Motion limits. The shipped values are 30000/9, so the sweep's
|
||||
# 5000 is a real reduction that must not leak into the next print.
|
||||
@@ -559,11 +724,26 @@ class FFPA:
|
||||
self._run('RESTORE_GCODE_STATE NAME=_ff_pa')
|
||||
except self.printer.command_error:
|
||||
pass
|
||||
# The app's resetTmcPa also replays SET_KINEMATIC_POSITION. That is
|
||||
# NOT ported and must not be: it needs it because its caller
|
||||
# force-drove the axes, while our sweep is plain G1 under Klipper's
|
||||
# own kinematics. Replaying it here would only lie to Klipper about
|
||||
# where the carriage is.
|
||||
# 5. LAST, and only now: tell Klipper where the carriage really is.
|
||||
# It planned the whole ladder against dead drivers, so its idea of
|
||||
# X and Y is the end of the ladder while the machine never left the
|
||||
# chute -- and the axes are still flagged homed, which is what
|
||||
# makes that dangerous rather than merely untidy. The app passes
|
||||
# exactly this string into resetTmcPa (@0x79350c).
|
||||
#
|
||||
# After RESTORE_GCODE_STATE, not before. SET_KINEMATIC_POSITION
|
||||
# fires toolhead:set_position, which has gcode_move recompute its
|
||||
# last_position from base_position/homing_position -- so it has to
|
||||
# see the RESTORED offsets, not the sweep's.
|
||||
if self.parked_at is not None and not self.dry_run:
|
||||
try:
|
||||
self._wait_moves()
|
||||
self._run('SET_KINEMATIC_POSITION X=%.3f Y=%.3f Z=%.3f'
|
||||
% self.parked_at)
|
||||
self._wait_moves()
|
||||
except self.printer.command_error:
|
||||
pass
|
||||
self.parked_at = None
|
||||
|
||||
def _heat(self, gcmd, tool, extruder):
|
||||
temp = gcmd.get_float('TEMP', None)
|
||||
@@ -578,17 +758,18 @@ class FFPA:
|
||||
|
||||
cmd_FF_PA_CALIBRATE_help = (
|
||||
"Measure pressure advance on the MOUNTED tool by sweeping candidates"
|
||||
" through the eBoard's scorer. Extrudes ~1 g in free air onto the"
|
||||
" plate and REPORTS a number -- it saves nothing and applies nothing"
|
||||
" ([TOOL=] [TEMP=] [SWEEPS=] [WINNERS=] [CANDIDATES=] [PASS_VALUE=]"
|
||||
" [Y_START=] [Y_STEP=] [Z=] [VERBOSE=1] [DRY_RUN=1])")
|
||||
" through the eBoard's scorer. Parks over the purge chute, extrudes"
|
||||
" ~1 g into it and REPORTS a number -- it saves nothing and applies"
|
||||
" nothing ([TOOL=] [TEMP=] [SWEEPS=] [WINNERS=] [CANDIDATES=]"
|
||||
" [PASS_VALUE=] [Y_START=] [Y_STEP=] [Z=] [PARK=0] [VERBOSE=1]"
|
||||
" [DRY_RUN=1])")
|
||||
|
||||
def cmd_FF_PA_CALIBRATE(self, gcmd):
|
||||
if self.running:
|
||||
raise gcmd.error("%s: a calibration is already running"
|
||||
% self.name)
|
||||
saved = (self.candidates, self.sweeps, self.winners,
|
||||
self.pass_value, self.y_start, self.y_step)
|
||||
self.pass_value, self.y_start, self.y_step, self.park)
|
||||
self.dry_run = bool(gcmd.get_int('DRY_RUN', 0, minval=0, maxval=1))
|
||||
verbose = bool(gcmd.get_int('VERBOSE', 0, minval=0, maxval=1))
|
||||
try:
|
||||
@@ -599,7 +780,7 @@ class FFPA:
|
||||
self.running = False
|
||||
self.dry_run = False
|
||||
(self.candidates, self.sweeps, self.winners, self.pass_value,
|
||||
self.y_start, self.y_step) = saved
|
||||
self.y_start, self.y_step, self.park) = saved
|
||||
|
||||
def _apply_overrides(self, gcmd):
|
||||
raw = gcmd.get('CANDIDATES', None)
|
||||
@@ -621,6 +802,8 @@ class FFPA:
|
||||
self.pass_value = gcmd.get_int('PASS_VALUE', self.pass_value)
|
||||
self.y_start = gcmd.get_float('Y_START', self.y_start)
|
||||
self.y_step = gcmd.get_float('Y_STEP', self.y_step)
|
||||
self.park = bool(gcmd.get_int('PARK', int(self.park),
|
||||
minval=0, maxval=1))
|
||||
if self.dry_run:
|
||||
# A dry run never collects a winner, so it would otherwise print
|
||||
# every sweep. One is what anyone wants to read.
|
||||
@@ -630,7 +813,7 @@ class FFPA:
|
||||
tool = self._resolve_tool(gcmd)
|
||||
self._check_homed(gcmd)
|
||||
self._check_filament(gcmd, tool)
|
||||
self._check_bounds(gcmd)
|
||||
self._check_bounds(gcmd, tool)
|
||||
self._ensure_extruder(tool)
|
||||
extruder = self._extruder()
|
||||
self._heat(gcmd, tool, extruder)
|
||||
@@ -642,10 +825,7 @@ class FFPA:
|
||||
lines = ["%s: T%d (%s), %d candidates x up to %d sweeps"
|
||||
% (self.name, tool, extruder.get_name(),
|
||||
len(self.candidates), self.sweeps),
|
||||
" lines are extruded in FREE AIR at Z %.3f raw / Z %.3f"
|
||||
" gcode. Nothing prints them and nothing inspects them --"
|
||||
" expect ~1 g of loose filament on the plate."
|
||||
% (raw_z, z_target)]
|
||||
self._where_note(tool, raw_z, z_target)]
|
||||
note = self._cross_section_note(extruder)
|
||||
if note:
|
||||
lines.append(note)
|
||||
@@ -654,7 +834,7 @@ class FFPA:
|
||||
extruder_name = extruder.get_name()
|
||||
with self._guarded(extruder):
|
||||
try:
|
||||
self._prologue(z_target)
|
||||
self._prologue(tool, z_target)
|
||||
best, census, rows = self._sweep(gcmd, extruder_name, verbose)
|
||||
except FFPAError as err:
|
||||
raise gcmd.error("%s: %s" % (self.name, err))
|
||||
@@ -721,7 +901,7 @@ class FFPA:
|
||||
"Draw ONE calibration line and print its eBoard verdict. The"
|
||||
" bring-up tool: run it across the candidates by hand and confirm"
|
||||
" you get more than one distinct answer before trusting an averaged"
|
||||
" number ([PA=] [Y=] [TOOL=] [TEMP=] [Z=] [DRY_RUN=1])")
|
||||
" number ([PA=] [Y=] [TOOL=] [TEMP=] [Z=] [PARK=0] [DRY_RUN=1])")
|
||||
|
||||
def cmd_FF_PA_PROBE(self, gcmd):
|
||||
if self.running:
|
||||
@@ -743,24 +923,38 @@ class FFPA:
|
||||
raise gcmd.error("%s: PA='%s' is not a number"
|
||||
% (self.name, pa_text))
|
||||
y = gcmd.get_float('Y', self.y_start)
|
||||
# PARK= is a per-invocation override of a persistent default, so it
|
||||
# must be undone on EVERY exit -- including a refusal from the checks
|
||||
# below, which run before _guarded gets a finally in place. Leaking it
|
||||
# would flip what the next FF_PA_CALIBRATE does with its gram.
|
||||
saved_park = self.park
|
||||
try:
|
||||
self.park = bool(gcmd.get_int('PARK', int(self.park),
|
||||
minval=0, maxval=1))
|
||||
self._probe_inner(gcmd, y, pa_text)
|
||||
finally:
|
||||
self.park = saved_park
|
||||
|
||||
def _probe_inner(self, gcmd, y, pa_text):
|
||||
tool = self._resolve_tool(gcmd)
|
||||
self._check_homed(gcmd)
|
||||
self._check_filament(gcmd, tool)
|
||||
self._check_bounds(gcmd)
|
||||
self._check_bounds(gcmd, tool)
|
||||
self._ensure_extruder(tool)
|
||||
extruder = self._extruder()
|
||||
self._heat(gcmd, tool, extruder)
|
||||
extruder = self._extruder()
|
||||
if not self.dry_run:
|
||||
self._check_can_extrude(gcmd, extruder)
|
||||
_raw_z, z_target = self._line_z(gcmd, tool)
|
||||
raw_z, z_target = self._line_z(gcmd, tool)
|
||||
extruder_name = extruder.get_name()
|
||||
gcmd.respond_info(self._where_note(tool, raw_z, z_target))
|
||||
# One line at the requested Y: index 0 with the ladder rebased.
|
||||
saved_y = self.y_start
|
||||
with self._guarded(extruder):
|
||||
try:
|
||||
self.y_start = y
|
||||
self._prologue(z_target)
|
||||
self._prologue(tool, z_target)
|
||||
verdict, unflushed = self._one_line(0, pa_text, extruder_name,
|
||||
True)
|
||||
except FFPAError as err:
|
||||
@@ -781,6 +975,18 @@ class FFPA:
|
||||
"Show the PA calibration config in force, whether the eBoard"
|
||||
" commands are bound, and the last run's table")
|
||||
|
||||
def _park_note(self):
|
||||
"""One line for FF_PA_STATUS. Deliberately not _where_note: that one
|
||||
needs a tool and a Z, and STATUS must answer with neither."""
|
||||
if not self.park:
|
||||
return "OFF -- the gram lands wherever the nozzle stands"
|
||||
ff = self.printer.lookup_object('gcode_macro _FF_FILAMENT', None)
|
||||
source = ("_FF_FILAMENT purge_x/purge_y + tool dx/dy"
|
||||
if ff is not None else "built-in defaults")
|
||||
if self.park_x is not None or self.park_y is not None:
|
||||
source = "park_x/park_y override"
|
||||
return "the purge chute, from %s" % source
|
||||
|
||||
def cmd_FF_PA_STATUS(self, gcmd):
|
||||
action = query = None
|
||||
bound = "no ([pa_adjust] not loaded)"
|
||||
@@ -799,6 +1005,7 @@ class FFPA:
|
||||
" ladder: X%.1f..%.1f, Y%.1f step %.1f"
|
||||
% (self.x_start, self.segments[-1][0], self.y_start,
|
||||
self.y_step),
|
||||
" park: %s" % self._park_note(),
|
||||
" eBoard commands bound: %s" % bound,
|
||||
" toolchanger: %s"
|
||||
% ("loaded" if self.toolchange is not None else "MISSING"),
|
||||
|
||||
Reference in new issue
Block a user