mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-23 05:42:17 -05:00
ff_print: optional automatic prepare, off by default
Adds [ff_print] (payload/klipper/extras/ff_print.py): it takes over
SDCARD_PRINT_FILE and M23, reads what the file says about itself, and calls
two ordinary macros -- FF_BEFORE_PRINT_START and FF_AFTER_PRINT_END -- before
chaining to the builtin. It holds no print policy; both macros live in
ff-print-macros.cfg and can be redefined.
This is the idea 1545ae1 reverted, hooked differently. That layer hooked the
steps piecemeal -- homing on SDCARD_PRINT_FILE, the Z offset on M109 -- and
what killed it was the bed: the stock block has no M190, so it would have
started on a cold one. This does not reproduce the sequence at scattered hook
points; it invokes the whole of START_PRINT at one point that runs before the
file's first line, so M190, the mesh, the post-heat G28 Z, the tool gate and
the clean all happen in order.
FF_BEFORE_PRINT_START.prepare is 0, so nothing changes: the shipped Orca
profile calls START_PRINT itself and stays the only path. Setting it to 1
suits a profile that prepares nothing -- FlashForge's stock block. It must NOT
be set for a profile that homes for itself: a generic Orca block runs G28
after M140/M104, and the G28 wrapper docks a mounted tool before homing Z, so
it would dock the tool the prepare just grabbed. That profile needs no
implicit prepare anyway -- it homes and waits for the bed on its own.
Metadata comes from the file's own commands, as the app's parser did (its
string table lists M104/M109/M140/M190/M141/M191 and ;HEIGHT: among the keys
it scanned): bed from the first M140/M190, nozzle from the first M104/M109,
initial tool from the first Tn, first-layer height from ;HEIGHT:. Nothing is
read from the slicer's config block, so there is no plate-type table and no
bed_temperature_formula to go stale, and no reason to read the far end of a
27 MB file. That route also has a trap: on a real file
`; first_layer_bed_temperature` read 55 where M140 said 80.
Per-tool clean temperatures are not in the file either. The app kept a
material per slot and looked the temperature up in its own table; that table
is already ported as _FF_FILAMENT.temps, so the missing half is now
_FF_FILAMENT.tool_material.
Independently of all that, START_PRINT loses its worst argument: TOOLS now
carries each tool's clean temperature as `0:220,2:240`, replacing the
parallel positional TEMPS= list and its index-alignment trap (TEMPS= still
accepted). The Orca snippet emits four {if is_extruder_used[n]} branches
instead of eight. _FF_REQUIRE_CALIBRATION and _FF_REQUIRE_TOOLS parsed the
same list and walked the same tools, so they merge into _FF_PREFLIGHT. The
SDCARD_PRINT_FILE macro override is gone -- two wrappers would fight over the
same rename -- and its flag resets and uncalibrated refusal moved into
FF_BEFORE_PRINT_START unchanged.
test-macros.py is 32 macro bodies over 7 files. Untested on hardware.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4705992627
commit
382fa5f2cb
6 files changed
+423
-66
No files matched your search
@@ -5,9 +5,11 @@ M106 P2 S0 ; Center_Fresch_Air_Input_Fan_for_PLA30%_(80_0-255)
|
||||
M106 S0 ; Model_Fan_(Heat_Break_Cooler_80-255)100%=255
|
||||
M106 P3 S0 ; Filter_Unit_Fan_0-255(ABS=255)
|
||||
M104 S[nozzle_temperature_initial_layer] T[initial_extruder] ; preheat in parallel (the clean re-sets it)
|
||||
; TOOLS = every extruder the file uses (presence gate + pre-print nozzle clean),
|
||||
; TEMPS = each tool's first-layer temperature, indexed by tool (0 = unused).
|
||||
START_PRINT BED=[bed_temperature_initial_layer_single] TOOL=[initial_extruder] NOZZLE=[nozzle_temperature_initial_layer] LAYER=[layer_height] LEVEL=0 TOOLS={if is_extruder_used[0]}0,{endif}{if is_extruder_used[1]}1,{endif}{if is_extruder_used[2]}2,{endif}{if is_extruder_used[3]}3{endif} TEMPS={if is_extruder_used[0]}{nozzle_temperature_initial_layer[0]}{else}0{endif},{if is_extruder_used[1]}{nozzle_temperature_initial_layer[1]}{else}0{endif},{if is_extruder_used[2]}{nozzle_temperature_initial_layer[2]}{else}0{endif},{if is_extruder_used[3]}{nozzle_temperature_initial_layer[3]}{else}0{endif} CLEAN=1
|
||||
; TOOLS = every extruder the file uses, each with its first-layer
|
||||
; temperature: the presence gate (refused before any heat or motion) and
|
||||
; the pre-print nozzle clean. Expanded per extruder because Orca has no
|
||||
; join over is_extruder_used.
|
||||
START_PRINT BED=[bed_temperature_initial_layer_single] TOOL=[initial_extruder] NOZZLE=[nozzle_temperature_initial_layer] LAYER=[layer_height] LEVEL=0 TOOLS={if is_extruder_used[0]}0:{nozzle_temperature_initial_layer[0]},{endif}{if is_extruder_used[1]}1:{nozzle_temperature_initial_layer[1]},{endif}{if is_extruder_used[2]}2:{nozzle_temperature_initial_layer[2]},{endif}{if is_extruder_used[3]}3:{nozzle_temperature_initial_layer[3]}{endif} CLEAN=1
|
||||
G90
|
||||
M83
|
||||
M109 S[nozzle_temperature_initial_layer] T[initial_extruder]
|
||||
|
||||
@@ -108,6 +108,11 @@ variable_temps: {'PLA': 220, 'PETG': 240, 'PLA-CF': 220, 'PETG-CF': 240,
|
||||
'TPU-64D': 220, 'PC': 260, 'PA': 260, 'PC-ABS': 260,
|
||||
'PPS-CF': 290, 'CUSTOM': 220}
|
||||
variable_default_temp: 220
|
||||
# What is loaded in each tool. The app kept the same thing per slot and looked
|
||||
# the operation temperature up in the table above (getFilamentOperationTemp);
|
||||
# nothing in the G-code file tells us, so it is configured here. Used by the
|
||||
# nozzle clean when START_PRINT is not given an explicit TEMPS=.
|
||||
variable_tool_material: ['PLA', 'PLA', 'PLA', 'PLA']
|
||||
# State shared between the helpers of one operation.
|
||||
variable_tool: -1
|
||||
variable_in_print: 0
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
# [include /usr/data/config/ff-filament.cfg]
|
||||
# [include /usr/data/config/ff-print-macros.cfg]
|
||||
#
|
||||
# Add [ff_print] (klippy-extras/ff_print.py) to have prints started from
|
||||
# Mainsail prepare the machine automatically, with no slicer changes at all:
|
||||
# [ff_print]
|
||||
# #before_macro: FF_BEFORE_PRINT_START
|
||||
# #after_macro: FF_AFTER_PRINT_END
|
||||
# #hook_commands: SDCARD_PRINT_FILE, M23
|
||||
# It must come after [virtual_sdcard], whose commands it wraps.
|
||||
#
|
||||
# Deliberate divergences from firmwareExe (documented in OKF/61):
|
||||
# * resume reheats all tools in parallel, not e1+e2 then e3+e4 staged
|
||||
# * no dmesg/meminfo dumps, no drop_caches shell calls
|
||||
@@ -37,13 +45,17 @@
|
||||
# see orca/machine-start-gcode.txt for the full snippet):
|
||||
# START_PRINT BED=[bed_temperature_initial_layer_single] TOOL=[initial_extruder] \
|
||||
# NOZZLE=[nozzle_temperature_initial_layer] LAYER=[layer_height] LEVEL=0 \
|
||||
# TOOLS=0,2 TEMPS=220,0,240,0 CLEAN=1
|
||||
# TOOLS every tool the file uses (comma list; Orca: is_extruder_used[n]).
|
||||
# TOOLS=0:220,2:240
|
||||
# TOOLS every tool the file uses, each with its clean temperature
|
||||
# ("0:220,2:240"; a bare "0,2" is fine and falls back to NOZZLE).
|
||||
# Each must be docked or mounted or the job is refused before any
|
||||
# heating/homing (app: E0165 "T: %d, no install"). Defaults to TOOL.
|
||||
# TEMPS per-tool clean temperature, indexed by tool number (0 or missing
|
||||
# = use NOZZLE). App: getFilamentOperationTemp = the slot's material
|
||||
# table temperature.
|
||||
# The clean temperature is the app's getFilamentOperationTemp, i.e.
|
||||
# the slot's material-table temperature.
|
||||
# TEMPS optional positional override of the clean temperatures, indexed by
|
||||
# tool number (0 = fall back). Without it each tool is cleaned at the
|
||||
# temperature its configured material maps to in the app's table --
|
||||
# _FF_FILAMENT.tool_material indexed into _FF_FILAMENT.temps.
|
||||
# CLEAN 1 (default): purge + wipe every tool in TOOLS while the bed heats
|
||||
# (app: clearNozzlePrint). 0 skips it.
|
||||
# SOAK seconds to dwell after the bed reaches target (app: 5 min). 0 = none.
|
||||
@@ -56,25 +68,35 @@
|
||||
# does NOT put the nozzle at the bed plane -- without this offset (~+3.2 mm
|
||||
# on this unit) the first layer is driven into the plate.
|
||||
# ---------------------------------------------------------------------------
|
||||
# Refuse BEFORE heating/homing/grabbing if the Z frame cannot be established.
|
||||
# The eddy-homed Z is ~3.2 mm low; with no nozzle_z/station_z the print-start
|
||||
# offset cannot be computed and the first layer goes into the plate.
|
||||
# TOOLS= (comma list) checks every listed tool; TOOL= a single one; neither
|
||||
# -> every tool must be calibrated.
|
||||
[gcode_macro _FF_REQUIRE_CALIBRATION]
|
||||
# _FF_PREFLIGHT TOOLS=<comma list> [TOOL=<n>]
|
||||
# Both pre-motion gates in one pass; refuses BEFORE any heating/homing/grab.
|
||||
# 1. Calibration -- the eddy-homed Z is ~3.2 mm low, so with no nozzle_z /
|
||||
# station_z the print-start offset cannot be computed and the first layer
|
||||
# goes into the plate. TOOLS= checks every listed tool, TOOL= adds one;
|
||||
# with neither, every tool must be calibrated. _FF_JOB.allow_uncalibrated
|
||||
# overrides this gate only.
|
||||
# 2. Tool presence (app: prepareForEddy @0x9f2084-0x9f2130, checkInLocation
|
||||
# -> "T: %d, no install" -> E0165). A tool is available when its dock
|
||||
# switch is pressed or it is the one on the carriage.
|
||||
[gcode_macro _FF_PREFLIGHT]
|
||||
gcode:
|
||||
{% set tool = params.TOOL|default(-1)|int %}
|
||||
{% set tools = [] %}
|
||||
{% for s in (params.TOOLS|default('')|string).split(',') %}
|
||||
{% if s|trim != '' %}{% set _ = tools.append(s|trim|int) %}{% endif %}
|
||||
{% if s|trim != '' and s|trim|int not in tools %}{% set _ = tools.append(s|trim|int) %}{% endif %}
|
||||
{% endfor %}
|
||||
{% if tool >= 0 and tool not in tools %}{% set _ = tools.append(tool) %}{% endif %}
|
||||
{% set tc = printer.ff_toolchange %}
|
||||
{% set missing = [] %}
|
||||
{% set uncalibrated = [] %}
|
||||
{% set absent = [] %}
|
||||
{% for t in tools %}
|
||||
{% if t not in tc.calibrated_tools %}{% set _ = missing.append(t) %}{% endif %}
|
||||
{% if t < 0 or t > 3 %}
|
||||
{ action_raise_error("TOOLS: tool %s out of range 0..3" % t) }
|
||||
{% endif %}
|
||||
{% if t not in tc.calibrated_tools %}{% set _ = uncalibrated.append(t) %}{% endif %}
|
||||
{% if t not in tc.docked_tools and t != tc.current_tool %}{% set _ = absent.append(t) %}{% endif %}
|
||||
{% endfor %}
|
||||
{% set ok = (tools|length > 0 and missing|length == 0 and tc.station_z is not none)
|
||||
{% set ok = (tools|length > 0 and uncalibrated|length == 0 and tc.station_z is not none)
|
||||
or (tools|length == 0 and tc.print_offset_ready) %}
|
||||
{% if not ok and not printer["gcode_macro _FF_JOB"].allow_uncalibrated %}
|
||||
{ action_raise_error("Not calibrated: station_z=%s, calibrated tools %s, needed %s. "
|
||||
@@ -83,32 +105,13 @@ gcode:
|
||||
"computed and the nozzle would be driven ~3 mm into the plate."
|
||||
% (tc.station_z, tc.calibrated_tools, tools)) }
|
||||
{% endif %}
|
||||
|
||||
# Tool-presence gate (app: prepareForEddy @0x9f2084-0x9f2130, checkInLocation
|
||||
# -> "T: %d, no install" -> E0165). A tool is available when its dock switch
|
||||
# is pressed or it is the one on the carriage. Refuses before any motion.
|
||||
[gcode_macro _FF_REQUIRE_TOOLS]
|
||||
gcode:
|
||||
{% set tools = [] %}
|
||||
{% for s in (params.TOOLS|default('')|string).split(',') %}
|
||||
{% if s|trim != '' %}{% set _ = tools.append(s|trim|int) %}{% endif %}
|
||||
{% endfor %}
|
||||
{% set tc = printer.ff_toolchange %}
|
||||
{% if tools|length > 0 and not tc.state_ok %}
|
||||
{ action_raise_error("Toolchanger state not ok: %s" % tc.state_reason) }
|
||||
{% endif %}
|
||||
{% set missing = [] %}
|
||||
{% for t in tools %}
|
||||
{% if t < 0 or t > 3 %}
|
||||
{ action_raise_error("TOOLS: tool %s out of range 0..3" % t) }
|
||||
{% elif t not in tc.docked_tools and t != tc.current_tool %}
|
||||
{% set _ = missing.append(t) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if missing|length > 0 %}
|
||||
{% if absent|length > 0 %}
|
||||
{ action_raise_error("Tool(s) %s not installed (dock switch not pressed, not mounted); "
|
||||
"docked %s, mounted T%s. Install them or re-slice without them."
|
||||
% (missing, tc.docked_tools, tc.current_tool)) }
|
||||
% (absent, tc.docked_tools, tc.current_tool)) }
|
||||
{% endif %}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -135,7 +138,9 @@ gcode:
|
||||
{% if s|trim != '' %}{% set _ = temps.append(s|trim|float) %}{% endif %}
|
||||
{% endfor %}
|
||||
{% for tool in tools %}
|
||||
{% set temp = temps[tool] if tool < temps|length and temps[tool] > 0 else fallback %}
|
||||
{% set mat = (ff.tool_material[tool]|upper) if tool < ff.tool_material|length else '' %}
|
||||
{% set by_material = ff.temps[mat] if mat in ff.temps else fallback %}
|
||||
{% set temp = temps[tool] if tool < temps|length and temps[tool] > 0 else by_material %}
|
||||
{% set extruder = printer["tool T%d" % tool].extruder %}
|
||||
{% set nozzle = printer.configfile.settings[extruder].nozzle_diameter|float %}
|
||||
{% set speed = ff.load_feed_thin if nozzle <= 0.25 else ff.load_feed %}
|
||||
@@ -164,28 +169,36 @@ gcode:
|
||||
{% set layer = params.LAYER|default(0)|float %}
|
||||
{% set clean = params.CLEAN|default(1)|int %}
|
||||
{% set soak = params.SOAK|default(0)|float %}
|
||||
; TOOLS takes "0,2" or, as the slicer emits it, "0:220,2:240" -- each used
|
||||
; tool with its own clean temperature. TEMPS= (positional, 0 = use NOZZLE)
|
||||
; is still accepted for files sliced before the paired form existed.
|
||||
{% set tools = [] %}
|
||||
{% set tmap = {} %}
|
||||
{% for s in (params.TOOLS|default('')|string).split(',') %}
|
||||
{% if s|trim != '' and s|trim|int not in tools %}{% set _ = tools.append(s|trim|int) %}{% endif %}
|
||||
{% if s|trim != '' %}
|
||||
{% set pair = s.split(':') %}
|
||||
{% set t = pair[0]|trim|int %}
|
||||
{% if t not in tools %}{% set _ = tools.append(t) %}{% endif %}
|
||||
{% if pair|length > 1 and pair[1]|trim != '' %}
|
||||
{% set _ = tmap.update({t: pair[1]|trim|float}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if tools|length == 0 and tool >= 0 %}{% set _ = tools.append(tool) %}{% endif %}
|
||||
{% set tool_list = tools|join(',') %}
|
||||
_FF_REQUIRE_CALIBRATION TOOL={tool}{% if tool_list %} TOOLS={tool_list}{% endif %}
|
||||
{% if tool_list %}
|
||||
_FF_REQUIRE_TOOLS TOOLS={tool_list} ; app: E0165 before any motion
|
||||
{% endif %}
|
||||
{% set temps = [tmap.get(0, 0), tmap.get(1, 0), tmap.get(2, 0), tmap.get(3, 0)]|join(',')
|
||||
if tmap else params.TEMPS|default('')|string|trim %}
|
||||
_FF_PREFLIGHT TOOL={tool}{% if tool_list %} TOOLS={tool_list}{% endif %}
|
||||
G90
|
||||
M82
|
||||
BED_MESH_CLEAR
|
||||
M400
|
||||
G28
|
||||
SET_GCODE_OFFSET X=0 Y=0 MOVE=1 MOVE_SPEED=600
|
||||
SET_IDLE_TIMEOUT TIMEOUT=1800000
|
||||
SET_IDLE_TIMEOUT TIMEOUT=864000 ; app value during print
|
||||
{% if bed > 0 %}
|
||||
M140 S{bed} ; app: heatManager(bed) -- heats during the clean
|
||||
{% endif %}
|
||||
{% if clean and tools|length > 0 %}
|
||||
{% set temps = params.TEMPS|default('')|string|trim %}
|
||||
_FF_NOZZLE_CLEAN TOOLS={tool_list}{% if temps %} TEMPS={temps}{% endif %} TEMP={nozzle}
|
||||
M106 P1 S0 ; app @0x9f27a4
|
||||
{% endif %}
|
||||
@@ -217,7 +230,6 @@ gcode:
|
||||
{% else %}
|
||||
TOOLCHANGE_SET_PRINT_OFFSET NOZZLE={nozzle} BED={bed} LAYER={layer}
|
||||
{% endif %}
|
||||
SET_IDLE_TIMEOUT TIMEOUT=864000 ; app value during print
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Common end-of-print block: the app's single exit sequence @0x7a25f0, run for
|
||||
@@ -268,6 +280,7 @@ gcode:
|
||||
SET_GCODE_OFFSET Z=0 MOVE=1
|
||||
M18
|
||||
SET_IDLE_TIMEOUT TIMEOUT=600
|
||||
SET_GCODE_VARIABLE MACRO=_FF_JOB VARIABLE=ended VALUE=1
|
||||
|
||||
# Slicer machine end gcode: just call END_PRINT
|
||||
[gcode_macro END_PRINT]
|
||||
@@ -293,6 +306,9 @@ variable_mainsail: 0
|
||||
# point both refuse on an uncalibrated machine). Only for deliberate tests:
|
||||
# SET_GCODE_VARIABLE MACRO=_FF_JOB VARIABLE=allow_uncalibrated VALUE=1
|
||||
variable_allow_uncalibrated: 0
|
||||
# Set by _FF_PRINT_END so the automatic FF_AFTER_PRINT_END does not run the
|
||||
# exit block a second time after END_PRINT or CANCEL_PRINT already did.
|
||||
variable_ended: 0
|
||||
gcode:
|
||||
|
||||
[gcode_macro CANCEL_PRINT]
|
||||
@@ -363,3 +379,92 @@ gcode:
|
||||
{% endif %}
|
||||
BASE_RESUME
|
||||
{% endif %}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Automatic print lifecycle -- the two entry points [ff_print] calls.
|
||||
#
|
||||
# ff_print.py wraps SDCARD_PRINT_FILE and M23, reads the slicer metadata out of
|
||||
# the file itself, and calls these macros. It holds no policy of its own:
|
||||
# change what happens around a print by redefining these, or by pointing
|
||||
# [ff_print] before_macro:/after_macro: at macros of your own. Everything it
|
||||
# derived is also readable as printer.ff_print.* (tools, nozzle_temps, tool,
|
||||
# nozzle, bed, layer, tools_param, filename, origin).
|
||||
#
|
||||
# FF_BEFORE_PRINT_START runs BEFORE the file is fed, which is the entire point:
|
||||
# the stock slicer profile's start G-code contains no G28, so its first move
|
||||
# (G1 Z5 F2400) requires the machine to be homed already. The touchscreen app
|
||||
# had the same ordering -- it prepared, then sent M23/M24.
|
||||
# ORIGIN SDCARD_PRINT_FILE (Moonraker/Mainsail) or M23 (touchscreen).
|
||||
# BED TOOL NOZZLE LAYER derived from the file's own M140/M104/Tn/;HEIGHT:;
|
||||
# passed through to START_PRINT verbatim, which ignores ORIGIN.
|
||||
# TOOLS is NOT derived -- nothing in the file names the tool set
|
||||
# without reading the slicer's config block, so a print prepared
|
||||
# this way cleans and gates the initial tool only (which is also
|
||||
# all the app checked at prepare time; the rest fail at grab).
|
||||
# Pass TOOLS= yourself for the stronger multi-tool gate.
|
||||
# ---------------------------------------------------------------------------
|
||||
[gcode_macro FF_BEFORE_PRINT_START]
|
||||
# OFF by default, so nothing changes for the shipped Orca profile: it calls
|
||||
# START_PRINT itself (assets/orca/machine-start-gcode.txt) and preparing here
|
||||
# as well would re-home and re-purge every tool. The print Z offset and T<n>
|
||||
# are both idempotent, so a double prepare wastes time and filament rather
|
||||
# than misplacing the first layer -- but there is no reason to pay it.
|
||||
#
|
||||
# Set to 1 only for a profile whose start block prepares NOTHING -- notably
|
||||
# FlashForge's stock one, which homes nowhere and assumes a machine the
|
||||
# touchscreen app already prepared. With this on, such a file homes, waits
|
||||
# for the bed, cleans and grabs before its first line. That is what the
|
||||
# reverted ff-orca-compat.cfg layer (1545ae1) could not do: it hooked the
|
||||
# steps piecemeal and never waited for the bed. Here the whole START_PRINT
|
||||
# runs, in order, before the file is fed.
|
||||
#
|
||||
# DO NOT set this for a profile whose start block homes for itself. A generic
|
||||
# Orca block does `G28` after its M140/M104, and our G28 wrapper docks a
|
||||
# mounted tool before homing Z -- so it would dock the tool this just grabbed
|
||||
# and print with an empty carriage. Such a profile needs no implicit prepare
|
||||
# anyway: it already homes and has its own M190. The toolchanger-specific
|
||||
# steps it still lacks (tool gate, nozzle clean, print Z offset, bed mesh) are
|
||||
# what an explicit START_PRINT call is for.
|
||||
#
|
||||
# Nor set it while the slicer profile calls START_PRINT itself: the print Z
|
||||
# offset and T<n> are idempotent, so a double prepare misplaces nothing, but
|
||||
# it re-homes and re-purges every tool for no reason.
|
||||
variable_prepare: 0
|
||||
# The stock touchscreen app runs its own prepare sequence before M23, so
|
||||
# preparing again there would repeat the homing, clean and grab. Set this to 1
|
||||
# if your screen does not prepare for itself.
|
||||
variable_prepare_on_m23: 0
|
||||
gcode:
|
||||
{% set origin = params.ORIGIN|default('')|string %}
|
||||
{% set me = printer["gcode_macro FF_BEFORE_PRINT_START"] %}
|
||||
{% if origin == 'SDCARD_PRINT_FILE' %}
|
||||
_FF_PREFLIGHT ; refuse an uncalibrated machine
|
||||
; before the file is even loaded,
|
||||
; as the old SDCARD_PRINT_FILE
|
||||
; wrapper did. START_PRINT calls
|
||||
; it again with the tool list.
|
||||
SDCARD_CLEAR_REFUELLING ; stale touchscreen flags -- see
|
||||
SDCARD_ENABLE_FFM ENABLE=0 ; ff-toolchange.cfg for why these
|
||||
SDCARD_SET_NEED_CHECK_EX CHECK=0 ; are load-bearing
|
||||
SDCARD_NO_FILAMENT_CHECK_EX CHECK=0
|
||||
SET_GCODE_VARIABLE MACRO=_FF_JOB VARIABLE=mainsail VALUE=1
|
||||
{% endif %}
|
||||
{% if not me.prepare %}
|
||||
; explicit path: the slicer profile calls START_PRINT itself
|
||||
{% elif origin == 'M23' and not me.prepare_on_m23 %}
|
||||
{ action_respond_info("ff_print: touchscreen print, the app prepares itself") }
|
||||
{% else %}
|
||||
START_PRINT {rawparams}
|
||||
{% endif %}
|
||||
|
||||
# FF_AFTER_PRINT_END -- called once the job leaves the printing state.
|
||||
# STATE complete | cancelled | error
|
||||
# The exit block is skipped when END_PRINT (slicer footer) or CANCEL_PRINT
|
||||
# already ran it -- both set _FF_JOB.ended themselves.
|
||||
[gcode_macro FF_AFTER_PRINT_END]
|
||||
gcode:
|
||||
{% if not printer["gcode_macro _FF_JOB"].ended %}
|
||||
_FF_PRINT_END
|
||||
{% endif %}
|
||||
SET_GCODE_VARIABLE MACRO=_FF_JOB VARIABLE=ended VALUE=0
|
||||
SET_GCODE_VARIABLE MACRO=_FF_JOB VARIABLE=mainsail VALUE=0
|
||||
@@ -209,22 +209,12 @@ stop_macro: MOTOR_STOP
|
||||
# remap flags). Those commands exist only in the fork; on the upstream
|
||||
# virtual_sdcard we now ship they are unknown commands. FIXME.
|
||||
# ---------------------------------------------------------------------------
|
||||
[gcode_macro SDCARD_PRINT_FILE]
|
||||
rename_existing: _FF_SDCARD_PRINT_FILE
|
||||
gcode:
|
||||
{% set tc = printer.ff_toolchange %}
|
||||
{% if not tc.print_offset_ready and not printer["gcode_macro _FF_JOB"].allow_uncalibrated %}
|
||||
{ action_raise_error("Refusing to print: station_z=%s, calibrated tools %s. "
|
||||
"Run STATION_CALIBRATE / TOOL_OFFSET_CALIBRATE (or FF_IMPORT_FIRMWARE_CONFIG) "
|
||||
"and SAVE_CONFIG first -- without them the nozzle would be driven ~3 mm into the plate."
|
||||
% (tc.station_z, tc.calibrated_tools)) }
|
||||
{% endif %}
|
||||
SDCARD_CLEAR_REFUELLING
|
||||
SDCARD_ENABLE_FFM ENABLE=0
|
||||
SDCARD_SET_NEED_CHECK_EX CHECK=0
|
||||
SDCARD_NO_FILAMENT_CHECK_EX CHECK=0
|
||||
SET_GCODE_VARIABLE MACRO=_FF_JOB VARIABLE=mainsail VALUE=1
|
||||
_FF_SDCARD_PRINT_FILE {rawparams}
|
||||
# MOVED: this wrapper is now [ff_print] (klippy-extras/ff_print.py), which
|
||||
# takes SDCARD_PRINT_FILE over in Python at klippy:connect and calls
|
||||
# FF_BEFORE_PRINT_START (ff-print-macros.cfg) before chaining to the builtin.
|
||||
# Two wrappers on one command would fight over the rename, so only one may
|
||||
# exist: the flag resets and the uncalibrated refusal that lived here are now
|
||||
# the SDCARD_PRINT_FILE branch of FF_BEFORE_PRINT_START, unchanged.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Homing Z with a head on the carriage rams the nozzle into the plate: Z
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
# Print-start / print-end entry points for the FlashForge Creator 5 Pro.
|
||||
#
|
||||
# Why this exists
|
||||
# The stock slicer profile's start G-code carries no toolchanger information
|
||||
# and, crucially, no G28 -- its only motion is `G1 Z5 F2400`. On an unhomed
|
||||
# machine that raises "Must home axis first", so the file cannot be the thing
|
||||
# that prepares the machine: something must home, clean and grab a tool
|
||||
# BEFORE the file's first line. The touchscreen app did exactly that -- it
|
||||
# ran its whole prepare sequence and only then sent M23/M24 (see
|
||||
# OKF/61-print-lifecycle-verified.md). It never subscribed to a print-start
|
||||
# event, because it *was* the thing starting the print.
|
||||
#
|
||||
# This module restores that ordering for prints started from Moonraker /
|
||||
# Mainsail, by wrapping the commands that begin a print. Doing it here
|
||||
# rather than in the slicer means the stock OrcaSlicer profile needs no
|
||||
# modification and already-sliced files keep working.
|
||||
#
|
||||
# Why not idle_timeout:printing
|
||||
# `idle_timeout:printing` is emitted from handle_sync_print_time (a
|
||||
# toolhead:sync_print_time handler), i.e. synchronously inside motion
|
||||
# scheduling -- not a safe place to run a G-code script, which is why
|
||||
# Klipper runs its own idle gcode from a reactor timer instead. It also
|
||||
# fires only once the toolhead syncs, which for a stock file is the very
|
||||
# `G1 Z5` that fails on unhomed axes: the event would arrive at the same
|
||||
# instant the print dies. There is no usable window.
|
||||
#
|
||||
# What this module does NOT do
|
||||
# It contains no print policy at all. It resolves the file, reads the
|
||||
# slicer metadata, and calls two ordinary G-code macros:
|
||||
#
|
||||
# FF_BEFORE_PRINT_START ORIGIN=<cmd> [BED=] [TOOL=] [NOZZLE=] [LAYER=]
|
||||
# [TOOLS=<tool>:<temp>,...]
|
||||
# FF_AFTER_PRINT_END STATE=<complete|cancelled|error|...>
|
||||
#
|
||||
# Both are defined in ff-print-macros.cfg and may be redefined by the user.
|
||||
# Everything derived from the file is also published in get_status, so a
|
||||
# redefined macro can read printer.ff_print.* instead of the parameters.
|
||||
#
|
||||
# What is read, and why this way
|
||||
# Only the head of the file, and only what the file states in its own
|
||||
# commands. The app's parser did the same -- its string table (firmwareExe
|
||||
# ~138069) lists M104/M109/M140/M190/M141/M191 and `;HEIGHT:` among the keys
|
||||
# it scanned, and the fields it produced: fisrNozzleIndex (sic), nozzleTemp,
|
||||
# bedTemp, chamberTemp, layerHeight.
|
||||
#
|
||||
# bed the file's first `M140`/`M190 S<t>`
|
||||
# nozzle the file's first `M104`/`M109 S<t>`
|
||||
# first tool the first bare `Tn` -- the file's initial extruder, NOT the
|
||||
# lowest-numbered one it uses; two otherwise identical files
|
||||
# here start on T0 and T2 respectively
|
||||
# layer the first `;HEIGHT:` -- the FIRST layer's height, which is
|
||||
# what the print Z offset's thin-layer term actually wants
|
||||
#
|
||||
# Nothing is read from the slicer's config block. That keeps this
|
||||
# slicer-agnostic: no table of plate types, no `bed_temperature_formula`,
|
||||
# nothing that breaks when a profile or slicer version changes, and no need
|
||||
# to read the far end of a 27 MB file. (The config-block route also has a
|
||||
# trap: on a real file `; first_layer_bed_temperature` read 55 where `M140`
|
||||
# said 80.)
|
||||
#
|
||||
# Per-tool clean temperatures are NOT taken from the file. The app kept a
|
||||
# material per slot and looked the temperature up in its own table; that
|
||||
# table is already ported as _FF_FILAMENT.temps, so the material per tool is
|
||||
# configured alongside it as _FF_FILAMENT.tool_material.
|
||||
#
|
||||
# All of the above sits within ~8 KB of the start of the file; HEAD_BYTES is a
|
||||
# wide margin around that, not a guess.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
EXTRUDER_COUNT = 4
|
||||
|
||||
# Bounded reads: measured offsets are ~8 KB (first Tn) and ~24 KB (config
|
||||
# block) on real files; these give a wide margin without loading a 27 MB file.
|
||||
HEAD_BYTES = 256 * 1024
|
||||
TAIL_BYTES = 256 * 1024
|
||||
|
||||
# print_stats states that mean the job is over (as opposed to paused mid-print).
|
||||
FINISHED_STATES = ('complete', 'cancelled', 'error')
|
||||
|
||||
|
||||
def _parse_metadata(path):
|
||||
"""Read what the file says about itself, from its own commands.
|
||||
|
||||
Returns a dict with whatever could be derived; missing keys simply are
|
||||
not present. Never raises -- a file we cannot read just yields {}, and
|
||||
the macro then runs with no derived parameters."""
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
head = f.read(HEAD_BYTES).decode('utf-8', 'replace')
|
||||
except Exception:
|
||||
logging.exception("ff_print: cannot read '%s'", path)
|
||||
return {}
|
||||
|
||||
meta = {}
|
||||
|
||||
# Bed and nozzle: the file's own first heat command, the way the app's
|
||||
# parser did it (it scanned M104/M109/M140/M190/M141/M191 and reported
|
||||
# nozzleTemp / bedTemp / chamberTemp).
|
||||
bed = re.search(r'^M1[49]0 S([0-9]+(?:\.[0-9]+)?)', head, re.M)
|
||||
if bed is not None and float(bed.group(1)) > 0:
|
||||
meta['bed'] = int(float(bed.group(1)))
|
||||
hot = re.search(r'^M10[49] S([0-9]+(?:\.[0-9]+)?)', head, re.M)
|
||||
if hot is not None and float(hot.group(1)) > 0:
|
||||
meta['nozzle'] = int(float(hot.group(1)))
|
||||
|
||||
# The file's initial extruder (the app's "fisrNozzleIndex", sic).
|
||||
first = re.search(r'^T([0-%d])\b' % (EXTRUDER_COUNT - 1), head, re.M)
|
||||
if first is not None:
|
||||
meta['tool'] = int(first.group(1))
|
||||
|
||||
# First-layer height, from the per-layer marker the slicer emits in the
|
||||
# body -- one of the three keys the app looked for, and the only one that
|
||||
# appears near the start of the file. This feeds the print Z offset's
|
||||
# thin-layer term, which is a FIRST-layer correction, so the first layer's
|
||||
# height is the value that belongs there.
|
||||
layer = re.search(r'^;HEIGHT:([0-9.]+)', head, re.M)
|
||||
if layer is not None:
|
||||
try:
|
||||
meta['layer'] = float(layer.group(1))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return meta
|
||||
|
||||
|
||||
class FFPrint:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.reactor = self.printer.get_reactor()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.before_macro = config.get('before_macro', 'FF_BEFORE_PRINT_START')
|
||||
self.after_macro = config.get('after_macro', 'FF_AFTER_PRINT_END')
|
||||
self.hook_commands = [c.strip().upper() for c
|
||||
in config.getlist('hook_commands',
|
||||
['SDCARD_PRINT_FILE', 'M23'])
|
||||
if c.strip()]
|
||||
self.prev_handlers = {}
|
||||
self.meta = {}
|
||||
self.filename = None
|
||||
self.origin = None
|
||||
# Latch: only a print WE announced may fire the end macro.
|
||||
self.active = False
|
||||
self.end_state = 'complete'
|
||||
self.end_timer = None
|
||||
self.printer.register_event_handler('klippy:connect',
|
||||
self._handle_connect)
|
||||
self.printer.register_event_handler('idle_timeout:ready',
|
||||
self._handle_ready)
|
||||
|
||||
def _handle_connect(self):
|
||||
# Same rename dance gcode_macro uses for rename_existing: take the
|
||||
# command over and keep the previous handler to chain to.
|
||||
for cmd in self.hook_commands:
|
||||
prev = self.gcode.register_command(cmd, None)
|
||||
if prev is None:
|
||||
raise self.printer.config_error(
|
||||
"ff_print: command '%s' is not registered -- [ff_print]"
|
||||
" must be loaded after [virtual_sdcard]" % (cmd,))
|
||||
self.prev_handlers[cmd] = prev
|
||||
self.gcode.register_command(
|
||||
cmd, self._make_handler(cmd),
|
||||
desc="%s (ff_print: runs %s first)"
|
||||
% (cmd, self.before_macro))
|
||||
self.end_timer = self.reactor.register_timer(self._run_end_macro)
|
||||
|
||||
def _make_handler(self, cmd):
|
||||
def handler(gcmd):
|
||||
self._cmd_start(cmd, gcmd)
|
||||
return handler
|
||||
|
||||
def get_status(self, eventtime):
|
||||
return {
|
||||
'filename': self.filename,
|
||||
'origin': self.origin,
|
||||
'active': self.active,
|
||||
'tool': self.meta.get('tool'),
|
||||
'nozzle': self.meta.get('nozzle'),
|
||||
'bed': self.meta.get('bed'),
|
||||
'layer': self.meta.get('layer'),
|
||||
}
|
||||
|
||||
def _resolve(self, gcmd, cmd):
|
||||
"""Path of the file this command is about, mirroring virtual_sdcard's
|
||||
own resolution (a plain join under sdcard_dirname)."""
|
||||
if cmd == 'M23':
|
||||
name = gcmd.get_raw_command_parameters().strip()
|
||||
else:
|
||||
name = gcmd.get('FILENAME', '')
|
||||
name = name.strip()
|
||||
if name.startswith('/'):
|
||||
name = name[1:]
|
||||
if not name:
|
||||
return None
|
||||
vsd = self.printer.lookup_object('virtual_sdcard', None)
|
||||
if vsd is None:
|
||||
return None
|
||||
return os.path.join(vsd.sdcard_dirname, name)
|
||||
|
||||
def _cmd_start(self, cmd, gcmd):
|
||||
path = self._resolve(gcmd, cmd)
|
||||
self.filename = path
|
||||
self.origin = cmd
|
||||
self.meta = _parse_metadata(path) if path else {}
|
||||
if self.meta:
|
||||
logging.info("ff_print: %s -> %s", path, self.meta)
|
||||
else:
|
||||
logging.info("ff_print: %s -> no slicer metadata found", path)
|
||||
|
||||
params = ['ORIGIN=%s' % (cmd,)]
|
||||
for key, name, fmt in (('bed', 'BED', '%d'), ('tool', 'TOOL', '%d'),
|
||||
('nozzle', 'NOZZLE', '%d'),
|
||||
('layer', 'LAYER', '%s')):
|
||||
if self.meta.get(key) is not None:
|
||||
params.append('%s=%s' % (name, fmt % (self.meta[key],)))
|
||||
|
||||
# Let the macro raise: a refusal here must stop the print BEFORE the
|
||||
# base command loads and resumes the file.
|
||||
self.gcode.run_script_from_command(
|
||||
'%s %s' % (self.before_macro, ' '.join(params)))
|
||||
# Arm the end latch only once prepare succeeded.
|
||||
self.active = True
|
||||
self.prev_handlers[cmd](gcmd)
|
||||
|
||||
def _handle_ready(self, print_time):
|
||||
"""idle_timeout:ready fires whenever the queue drains -- including a
|
||||
long M190 mid-print and any manual jog. Only a job we announced, and
|
||||
only one print_stats calls finished, is a real end of print."""
|
||||
if not self.active:
|
||||
return
|
||||
stats = self.printer.lookup_object('print_stats', None)
|
||||
if stats is None:
|
||||
return
|
||||
state = stats.get_status(self.reactor.monotonic()).get('state')
|
||||
if state not in FINISHED_STATES:
|
||||
return
|
||||
self.active = False
|
||||
self.end_state = state
|
||||
# Run the macro from a timer, not from this event: the handler runs in
|
||||
# the idle_timeout timeout path and should not block on a G-code script.
|
||||
self.reactor.update_timer(self.end_timer, self.reactor.NOW)
|
||||
|
||||
def _run_end_macro(self, eventtime):
|
||||
state = self.end_state
|
||||
try:
|
||||
self.gcode.run_script('%s STATE=%s' % (self.after_macro, state))
|
||||
except Exception:
|
||||
logging.exception("ff_print: %s failed", self.after_macro)
|
||||
return self.reactor.NEVER
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return FFPrint(config)
|
||||
@@ -1278,7 +1278,7 @@ class FFToolchange:
|
||||
'print_offset_ready': self.print_offset_ready(),
|
||||
# Tools currently sitting in their docks (dock switch
|
||||
# pressed). A tool is available for a print when it is
|
||||
# docked or is the mounted one (_FF_REQUIRE_TOOLS).
|
||||
# docked or is the mounted one (_FF_PREFLIGHT).
|
||||
'docked_tools': [i for i in range(EXTRUDER_COUNT)
|
||||
if self._in_location(i, eventtime)],
|
||||
# Tool whose runout/clog sensors are enabled (-1 = none)
|
||||
|
||||
Reference in new issue
Block a user