Initial import of my Klipper macros, modules and scripts

Macros as they run on the printer (Voron 2.4 + AFC Box Turtle +
Blobifier + Beacon + bd_pressure + bdwidth + iHeater), the Klipper
modules they need (xy_drift, bdwidth_status, bdpressure_check), MCU
firmware update scripts, install.sh with Moonraker update manager
support, and READMEs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-10 13:45:11 +02:00
commit 6dbb4ca3fb
24 files changed
+3906

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
#####################################################################
## XY lost-step check (needs xy_drift.py + [xy_drift])
#####################################################################
# Homing resets the toolhead position, so the position after G28 cannot
# show lost steps. [xy_drift] compares the stepper step counts at the X and
# Y endstop triggers instead: every lost step shows up as drift.
# _AFC_STORE_HOME_REFERENCE (PRINT_START) stores the reference, every
# AFC_XY_SAFETY_CHECK compares against it and then stores a new reference,
# so each check reports the drift since the previous one.
[xy_drift]
[gcode_macro _AFC_STORE_HOME_REFERENCE]
gcode:
G28 X Y
M400
XY_DRIFT_STORE
[gcode_macro AFC_XY_SAFETY_CHECK]
description: Re-home XY and report steps lost since the last check
variable_allowed_mm: 0.1 # one motor skipping one cycle (0.4 mm) shows as 0.2 mm in X and Y; endstop noise ~0.02
variable_soft_fail: 1 # 1 = PAUSE on drift, 0 = M112
gcode:
{% set th = printer.toolhead %}
M117 AFC XY check...
SAVE_GCODE_STATE NAME=_AFC_XY_CHECK
G90
G1 Z{[th.position.z + 10, th.axis_maximum.z]|min} F3000
G28 X Y
M400
_AFC_CHECK_CAPTURE
RESTORE_GCODE_STATE NAME=_AFC_XY_CHECK
[gcode_macro _AFC_CHECK_CAPTURE]
gcode:
{% set d = printer.xy_drift %}
{% set cfg = printer["gcode_macro AFC_XY_SAFETY_CHECK"] %}
{% set th = printer.toolhead %}
{% set lim = cfg.allowed_mm|float %}
{% set drift = d.ref_set and (d.x|abs > lim or d.y|abs > lim) %}
{% if not d.ref_set %}
RESPOND PREFIX="AFC" MSG="XY check: no reference yet (Klipper restarted?), storing one now"
{% else %}
RESPOND PREFIX="AFC" MSG="XY drift since last check: X={'%.3f' % d.x} Y={'%.3f' % d.y} mm (limit {lim})"
{% endif %}
# New reference from this homing, so the next check reports only new drift
XY_DRIFT_STORE
{% if not drift %}
{% if d.ref_set %}
RESPOND PREFIX="AFC" MSG="Position OK"
{% endif %}
G1 X{th.axis_maximum.x - 10} Y{th.axis_maximum.y - 10} F5000
{% elif cfg.soft_fail|int == 1 %}
RESPOND TYPE=error MSG="AFC: XY drift detected! X={'%.3f' % d.x} Y={'%.3f' % d.y} mm. Axes are re-homed, print PAUSED. Run AFC_XY_RESUME to continue, or CANCEL_PRINT."
M117 AFC: XY drift! See console. Paused.
PAUSE
{% else %}
RESPOND TYPE=error MSG="AFC: XY drift X={'%.3f' % d.x} Y={'%.3f' % d.y} mm -> M112"
M112
{% endif %}
[gcode_macro AFC_XY_RESUME]
description: Acknowledge XY drift warning and resume print
gcode:
RESPOND PREFIX="AFC" MSG="User acknowledged drift, resuming."
# Axes were re-homed by the check, so AFC's resume returns to the
# correct print position.
RESUME
#####################################################################
## unloading help
#####################################################################
[gcode_macro LANE_FEED_UNTIL_HUB_OPEN]
variable_lane: 1
variable_step: -50
variable_max_dist: 2000
variable_moved: 0
variable_active: 0
gcode:
{% set lane = params.LANE|default(1)|int %}
{% set step = params.STEP|default(-50)|float %}
{% set max_dist = params.MAX_DIST|default(2000)|float %}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=lane VALUE={lane}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=step VALUE={step}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=max_dist VALUE={max_dist}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=moved VALUE=0
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=1
RESPOND PREFIX="info" MSG="START lane={lane} step={step} max={max_dist}"
UPDATE_DELAYED_GCODE ID=_lane_feed_loop DURATION=0.05
[delayed_gcode _lane_feed_loop]
gcode:
{% set m = printer["gcode_macro LANE_FEED_UNTIL_HUB_OPEN"] %}
{% if m.active == 1 %}
LANE_MOVE LANE=lane{m.lane} DISTANCE={m.step}
{% set new_moved = m.moved + m.step %}
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=moved VALUE={new_moved}
RESPOND PREFIX="info" MSG="MOVE lane={m.lane} step={m.step} moved={new_moved}"
QUERY_ENDSTOPS
UPDATE_DELAYED_GCODE ID=_lane_eval DURATION=0.03
{% else %}
RESPOND PREFIX="info" MSG="feed_loop inactive"
{% endif %}
[delayed_gcode _lane_eval]
gcode:
{% set m = printer["gcode_macro LANE_FEED_UNTIL_HUB_OPEN"] %}
{% if m.active == 1 %}
{% set hub = printer.query_endstops.last_query.get("hub", "unknown") %}
# RESPOND PREFIX="info" MSG="QUERY hub={hub} raw={printer.query_endstops.last_query.get('hub', 'MISSING')}"
# RESPOND PREFIX="info" MSG="last_query: {printer.query_endstops.last_query}"
{% if hub == "open" or hub == 0 %}
RESPOND PREFIX="info" MSG="HUB TRIGGERED → STOP"
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=0
LANE_MOVE LANE=lane{m.lane} DISTANCE=-100
{% elif m.moved|abs >= m.max_dist %}
RESPOND PREFIX="warning" MSG="MAX DIST STOP reached={m.moved}"
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=0
{% else %}
RESPOND PREFIX="info" MSG="CONTINUE"
UPDATE_DELAYED_GCODE ID=_lane_feed_loop DURATION=0.03
{% endif %}
{% else %}
RESPOND PREFIX="info" MSG="eval inactive"
{% endif %}
[gcode_macro LANE_FEED_ABORT]
gcode:
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=0
UPDATE_DELAYED_GCODE ID=_lane_feed_loop DURATION=0
UPDATE_DELAYED_GCODE ID=_lane_eval DURATION=0
RESPOND PREFIX="warning" MSG="ABORTED"
[gcode_macro HOME]
gcode:
G90
G4 P2000
G28 X0 Y0 Z0
G1 X175 Y175 Z10 F1200
[gcode_macro SET_ACTIVE_SPOOL]
gcode:
{% if params.ID %}
{% set id = params.ID|int %}
{action_call_remote_method(
"spoolman_set_active_spool",
spool_id=id
)}
{% else %}
{action_respond_info("Parameter 'ID' is required")}
{% endif %}
[gcode_macro CLEAR_ACTIVE_SPOOL]
gcode:
{action_call_remote_method(
"spoolman_set_active_spool",
spool_id=None
)}
# [gcode_macro TEST_AMS_STRESS]
# gcode:
# {% for i in range(500) %}
# T0 ; Load tool 0
# G4 P5000 ; Wait 5s
# T1 ; Unload
# G4 P3000 ; Wait 3s
# {% endfor %}
+654
View File
@@ -0,0 +1,654 @@
########################################################################
# BD Pressure Advance x AFC BoxTurtle x Spoolman Integration
# ---------------------------------------------------------------
# Built and verified using ONLY documented/source-confirmed APIs:
#
# bd_pressure (https://github.com/markniu/bd_pressure):
# PA_CALIBRATE NOZZLE_TEMP=<t> MAX_VOLUMETRIC=<v> ACC_WALL=<a>
# TRAVEL_SPEED=<s> ACC_TO_DECEL_FACTOR=<pct>
# - filament must already be loaded into the nozzle before calling
# - the command itself homes, heats, calibrates, and calls
# SET_PRESSURE_ADVANCE -- no separate heating step is needed
# Source: https://pandapi3d.cn/bdpressure/home
#
# AFC-Klipper-Add-On (https://github.com/ArmoredTurtle/AFC-Klipper-Add-On):
# printer.AFC.current_load -- a live status field (confirmed in
# klippy/extras/AFC.py get_status(): str['current_load'] = self.current)
# that always reflects whichever lane is currently loaded in the
# toolhead, regardless of how it got there (T0-T4, CHANGE_TOOL,
# BT_CHANGE_TOOL, or AFC's own infinite-spool runout remapping).
# Confirmed working live on this printer via a test macro.
# SET_SPOOL_ID LANE=.. SPOOL_ID=.. -- assigns a spool, AFC then pulls
# material/color/weight/temps from Spoolman.
# Source: https://www.armoredturtle.xyz/docs/afc-klipper-add-on/
# and confirmed directly against installed AFC source.
#
# Klipper core (https://www.klipper3d.org/Command_Templates.html):
# [save_variables] + SAVE_VARIABLE / printer.save_variables.variables
# -- persists the PA cache and lane->spool_id tracking across
# restarts. No shell_command/curl/Moonraker REST calls needed.
# [delayed_gcode] + UPDATE_DELAYED_GCODE -- used to poll
# printer.AFC.current_load every 2 seconds and detect tool changes
# (see "WHY POLLING" below for why this is the mechanism, instead
# of intercepting a tool-change command directly).
#
# WHY POLLING INSTEAD OF INTERCEPTING A TOOL-CHANGE COMMAND
# ---------------------------------------------------------------------
# This was not the first design tried. Three other approaches were
# attempted and ruled out, each one verified directly against this
# printer's installed AFC Python source (not just AFC's docs, which
# turned out to describe behavior that didn't match the installed
# version in two separate cases):
#
# 1. AFC_POST_PREP (an AFC-documented "fires after every lane load"
# macro hook): verified against source (AFC_lane.py) that this is
# tied to a lane's prep+load SENSOR transition (filament freshly
# inserted into the physical lane) and to an unset-by-default
# `post_prep_macro` config option -- not to a tool change moving
# filament into the toolhead. An empirical test on a real tool
# change confirmed it never fires for that case.
#
# 2. Wrapping CHANGE_TOOL with rename_existing: CHANGE_TOOL the gcode
# command is real (registered via register_command in AFC.py) and
# the wrapper DOES work when CHANGE_TOOL is called by that exact
# name -- confirmed working live. But T0-T4 (what slicers and
# normal use actually send) do NOT route through it: verified in
# AFC_functions.py that T(n) commands are registered with
# `self.afc.gcode.register_command(cur_lane.map, self.afc.cmd_CHANGE_TOOL, ...)`
# -- a direct reference to the original Python method captured at
# registration time. Renaming the "CHANGE_TOOL" dispatcher key has
# zero effect on these separately-registered "T0".."T4" keys.
# Confirmed empirically: CHANGE_TOOL LANE=lane1 typed directly
# applied cached PA correctly; T1 typed directly did not.
#
# 3. Wrapping T0-T4 directly with rename_existing: ruled out for two
# reasons. First, T(n) commands are registered dynamically at PREP
# time (a runtime event), not at config-load time, so
# rename_existing (config-load-time only) can't reliably target
# them. Second, even if that were solved, AFC's SET_MAP /
# RESET_AFC_MAPPING / infinite-spool-runout features rely on which
# lane a given T(n) points to being mutable at runtime -- so
# statically pinning T0-T4 via `map:` in lane config would fight
# that real, separate AFC feature.
#
# Given all of that, polling printer.AFC.current_load (a plain status
# read with no dependency on which command triggered the change) is the
# most robust mechanism available: it works identically regardless of
# whether a tool change was triggered by T0-T4, CHANGE_TOOL,
# BT_CHANGE_TOOL, or AFC's internal runout remapping, and needs no
# load-order considerations at all. The cost is a small polling delay
# (up to ~2 seconds) between a tool change finishing and PA being
# applied, instead of it happening in the same instant.
#
# OTHER PAST MISTAKES, FOR REFERENCE
# ---------------------------------------------------------------------
# - "BD_SENSOR_CALIBRATE_PA" is not a real command. Real: PA_CALIBRATE,
# with required parameters (see above).
# - "printer.afc.lanes[lane].spool_id" is not a real, readable Klipper
# status path. AFC's per-lane spool data partly lives in Moonraker's
# database (namespace "lane_data"), which a Klipper gcode_macro
# cannot reach directly (no HTTP from Jinja2 templates). This file
# tracks lane->spool_id itself instead (see _PA_TRACK_LANE_SPOOL).
# - SET_SPOOL_ID is NOT wrapped with rename_existing, because it is
# registered via register_command (confirmed against installed AFC
# source) and wrapping it failed with "Existing command 'SET_SPOOL_ID'
# not found in gcode_macro rename" when this file's [include] line
# in printer.cfg appeared BEFORE AFC's own includes. The
# companion-macro approach (_PA_TRACK_LANE_SPOOL, called manually
# right after SET_SPOOL_ID) has no such load-order dependency, at
# the small cost of one extra line each time you assign a spool.
# - Calibration nozzle temp comes from printer.extruder.target (a
# documented Klipper status field) at calibration time. AFC already
# sets this from Spoolman's nozzle_temp field when a lane is loaded
# (AFC's own documented "Setting extruder temp" feature), so this
# answers "can the calibration temp come from Spoolman" with a real
# yes, via a path that needs no new plumbing.
#
# KNOWN LIMITATIONS / THINGS TO VERIFY ON YOUR PRINTER
# ---------------------------------------------------------------------
# 1) You must remember to call _PA_TRACK_LANE_SPOOL LANE=.. SPOOL_ID=..
# every time you assign a spool to a lane (right after SET_SPOOL_ID,
# or after assigning via the Mainsail/Fluidd spool UI). If you skip
# this, LANE_CALIBRATE_PA and the auto-apply poller will report
# "no spool_id recorded yet" for that lane. PA_STATUS shows you which
# lanes are missing this.
#
# 2) PA is applied up to ~2 seconds after a tool change completes, not
# instantly, because of the polling interval. If you need it tighter,
# lower the DURATION values in _PA_POLL_LANE_CHANGE below (the
# initial_duration and both UPDATE_DELAYED_GCODE DURATION values),
# at the cost of slightly more frequent background checks.
#
# 3) If printer.AFC ever doesn't exist (AFC failed to load, or a future
# AFC version renames its status object), _PA_POLL_LANE_CHANGE will
# print one warning message and then go silent -- it will NOT spam
# the console, and it will NOT apply PA automatically until this is
# resolved. PA_STATUS and manual LANE_CALIBRATE_PA calls still work
# fine in that case; only the automatic apply-on-tool-change part is
# affected.
# REQUIRED SETUP
# ---------------------------------------------------------------------
# 1) [save_variables] is declared below in this same file (see the
# [save_variables] section right after this header) -- you do NOT
# need to add it to printer.cfg separately.
# IMPORTANT: if you already have a [save_variables] section
# somewhere else in your config (printer.cfg or another include),
# DELETE the one below -- Klipper will error on a duplicate section
# if it appears twice anywhere in your merged config. Only one
# [save_variables] section is allowed printer-wide, but it stores
# ALL variables from ALL macros in one shared file, so having it
# declared once, here, works fine for everything in your setup.
#
# 2) Make sure moonraker.conf has [spoolman] configured so AFC can pull
# nozzle_temp/material from Spoolman when a lane is loaded:
# [spoolman]
# server: http://<spoolman_ip>:7912
# sync_rate: 5
#
# 3) Add to printer.cfg:
# [include bd_pa_afc_spoolman.cfg]
# (Include order relative to AFC no longer matters for this version
# -- the polling approach has no load-order dependency, unlike the
# CHANGE_TOOL/SET_SPOOL_ID wrapper attempts described above.)
#
# 4) Whenever you assign a spool to a lane -- via SET_SPOOL_ID, or via
# the Mainsail/Fluidd spool-assignment UI -- also call
# _PA_TRACK_LANE_SPOOL with the same lane and spool_id, e.g.:
# SET_SPOOL_ID LANE=lane1 SPOOL_ID=12345
# _PA_TRACK_LANE_SPOOL LANE=lane1 SPOOL_ID=12345
# This is what lets the PA cache know which spool_id is in which
# lane (see KNOWN LIMITATIONS #1 above for why this is manual).
#
# USAGE
# ---------------------------------------------------------------------
# After assigning spools to lanes (step 4 above), in PRINT_START,
# after your PREP call, run:
# PA_CALIBRATE_ALL_LANES LANES="lane1,lane2,lane3,lane4,lane5"
# This calibrates (or restores from cache) PA for every lane listed
# that currently has a spool_id recorded.
#
# Per-lane PA is then applied automatically within ~2 seconds of every
# tool change, via the _PA_POLL_LANE_CHANGE background poller -- no
# action needed on your part once it's running.
#
# To force a re-calibration of one lane:
# LANE_RECAL_PA LANE=lane2
#
# To see what's cached, and which lanes are missing a tracked
# spool_id:
# PA_STATUS
########################################################################
# ---------------------------------------------------------------------------
# Persistent variable storage -- required by SAVE_VARIABLE/printer.save_variables
# used throughout this file for the PA cache and lane->spool_id tracking.
# DELETE THIS SECTION if you already have a [save_variables] section
# defined anywhere else in your config (only one is allowed printer-wide).
# ---------------------------------------------------------------------------
[save_variables]
filename: ~/printer_data/config/bd_pa_variables.cfg
# The file above does not need to exist yet -- Klipper creates it on
# first SAVE_VARIABLE call and reloads it on every restart.
# ---------------------------------------------------------------------------
# USER CONFIGURATION -- edit these to match your printer
# ---------------------------------------------------------------------------
[gcode_macro _PA_CFG]
description: Central config -- edit values here
variable_lane_names: "lane1,lane2,lane3,lane4,lane5"
# Fallback nozzle temp (C) used ONLY if the extruder's target temp reads 0
# at calibration time (e.g. you called this macro before AFC set a temp)
variable_fallback_nozzle_temp: 260
# bd_pressure PA_CALIBRATE parameters -- tune to your printer/filament.
# See https://pandapi3d.cn/bdpressure/home for guidance on these values.
variable_max_volumetric: 25
variable_acc_wall: 5000
variable_travel_speed: 300
variable_acc_to_decel_factor: 50
# Minimum PA delta before we bother re-saving the cached value
variable_write_threshold: 0.002
# Spoolman URL and the custom "extra" field name used to push calibrated
# PA values back to the spool record (so the value travels with the
# spool, not just with this printer's local save_variables cache).
# Create the field once with: _PA_SETUP_SPOOLMAN_FIELD (see below).
variable_spoolman_url: "http://localhost:7912"
variable_spoolman_field: "pa_value"
gcode: # intentionally empty -- variables only
# ---------------------------------------------------------------------------
# Spoolman write-back: push a calibrated PA value to a spool's "extra"
# field, so the value travels with the spool (e.g. if you move it to a
# different printer, or reinstall save_variables from scratch).
#
# Requires the custom field to exist once -- run _PA_SETUP_SPOOLMAN_FIELD
# (below) a single time, or create it manually:
# curl -X POST http://<spoolman_ip>:7912/api/v1/field/spool/pa_value -H "Content-Type: application/json" -d '{"name": "Pressure Advance", "field_type": "float", "default_value": "0"}'
#
# UNVERIFIED -- needs live testing the same way everything else in this
# file has needed it. In particular: confirm curl and python3 are both
# available in the shell shell_command runs in (they're standard on
# MainsailOS/FluiddOS, but worth confirming), and confirm the merge-write
# below doesn't clobber any other custom Spoolman fields you may have.
# ---------------------------------------------------------------------------
[gcode_shell_command _pa_write_spoolman]
# HISTORICAL NOTE ON ARGUMENT OFFSET (no longer applicable here, kept
# for context): an earlier version of this command ran the script
# inline via `bash -c '<script>' arg1 arg2 ...`, which makes the FIRST
# argument after the script string become $0 inside the script, not $1
# -- a well-known bash gotcha, confirmed empirically while building
# this file:
# bash -c 'echo "0=$0 1=$1 2=$2"' alpha beta gamma
# -> 0=alpha 1=beta 2=gamma (gamma, the 3rd arg, never reaches $3)
# This command now invokes a real .sh FILE instead (`bash script.sh
# arg1 arg2 ...`), which does NOT have this offset -- $1 really is the
# first argument in that invocation form (also confirmed empirically).
# So RUN_SHELL_COMMAND ... PARAMS="a b c d" maps cleanly to $1-$4 in
# the script with no placeholder needed.
#
# WHY THIS CALLS AN EXTERNAL .sh FILE INSTEAD OF EMBEDDING PYTHON
# INLINE: Klipper's config parser (Python's configparser) requires
# EVERY physical line of a multi-line value to be indented, and strips
# that indentation uniformly before passing the value to the shell.
# Confirmed empirically while building this file: this dedenting
# flattens ALL lines to the same level, which destroys the relative
# indentation Python's try/except blocks need to remain valid syntax --
# there is no way to embed multi-line, block-structured Python directly
# inside a Klipper command: value. Putting the logic in a separate .sh
# file sidesteps this entirely, since that file is never touched by
# Klipper's config parser.
#
# The script ships in this folder (klipper-macros/bd_pa_write_spoolman.sh);
# install.sh makes it executable.
command: bash ~/printer_data/config/klipper-macros/bd_pa_write_spoolman.sh
verbose: False
[gcode_shell_command _pa_create_spoolman_field]
# Same reasoning as _pa_write_spoolman above for using an external
# script file -- see the note there. This one has no embedded Python,
# so the only real issue was the closing quote sitting at column 0
# (un-indented), which is also a config parsing error in Klipper's
# parser -- every line of a multi-line value must be indented,
# including the very last line.
command: bash ~/printer_data/config/klipper-macros/bd_pa_create_spoolman_field.sh
verbose: False
[gcode_macro _PA_SETUP_SPOOLMAN_FIELD]
description: One-time setup -- creates the pa_value custom field in Spoolman
# REQUIRES bd_pa_create_spoolman_field.sh to already exist on disk at
# ~/printer_data/config/ -- see the one-time SSH setup instructions
# given alongside this file. This macro just invokes that script; it
# does not create it.
gcode:
{% set cfg = printer["gcode_macro _PA_CFG"] %}
{action_respond_info("Creating Spoolman field '%s' at %s ..." % (cfg.spoolman_field, cfg.spoolman_url))}
RUN_SHELL_COMMAND CMD=_pa_create_spoolman_field PARAMS="{cfg.spoolman_url} {cfg.spoolman_field}"
{action_respond_info("Done. Verify with: curl %s/api/v1/field/spool" % cfg.spoolman_url)}
# ---------------------------------------------------------------------------
# Lane -> spool_id tracking
# ---------------------------------------------------------------------------
# AFC's own per-lane spool_id lives in Moonraker's database, which a
# Klipper gcode_macro cannot read directly (no HTTP from Jinja2). Instead,
# we track it ourselves with a small companion macro, _PA_TRACK_LANE_SPOOL.
#
# Two ways to keep it updated:
#
# A) AUTOMATIC -- the [gcode_macro SET_SPOOL_ID] wrapper below uses
# `rename_existing` to intercept AFC's real SET_SPOOL_ID command,
# call _PA_TRACK_LANE_SPOOL itself, then call through to AFC's
# original implementation. If this loads without error, you don't
# need to do anything extra -- just keep using SET_SPOOL_ID exactly
# as you already do (typed manually, or via Mainsail/Fluidd's UI,
# if that UI calls SET_SPOOL_ID under the hood -- unconfirmed).
#
# B) MANUAL -- call _PA_TRACK_LANE_SPOOL yourself right after every
# SET_SPOOL_ID:
# SET_SPOOL_ID LANE=lane1 SPOOL_ID=12345
# _PA_TRACK_LANE_SPOOL LANE=lane1 SPOOL_ID=12345
#
# IMPORTANT -- THIS NEEDS THE SAME LIVE VERIFICATION CHANGE_TOOL GOT:
# An earlier version of this file tried wrapping SET_SPOOL_ID the same
# way and got "Existing command 'SET_SPOOL_ID' not found in gcode_macro
# rename" -- but that was BEFORE this file's [include] line was moved to
# load after AFC's includes. The CHANGE_TOOL wrapper loaded cleanly after
# that fix, but later testing found CHANGE_TOOL's wrapper doesn't catch
# T0-T4 (a separate, real problem specific to how AFC registers per-lane
# tool-change commands -- see the WHY POLLING section near the top of
# this file). SET_SPOOL_ID has NOT been tested live the way CHANGE_TOOL
# was. It may load cleanly now and work correctly every time -- or it may
# load cleanly and still silently miss some assignment path (e.g. if the
# Mainsail/Fluidd spool-picker UI doesn't actually call SET_SPOOL_ID
# under the hood, calling some other AFC function instead). After adding
# this, test it the same way CHANGE_TOOL was tested: assign a spool, then
# immediately run PA_STATUS and confirm the new spool_id actually shows
# up WITHOUT having called _PA_TRACK_LANE_SPOOL manually. If it doesn't,
# fall back to option B (manual call) for whatever assignment path
# didn't get caught.
# ---------------------------------------------------------------------------
[gcode_macro SET_SPOOL_ID]
description: Wraps AFC's SET_SPOOL_ID to also track lane->spool_id for PA caching
rename_existing: _PA_SET_SPOOL_ID_BASE
gcode:
{% set lane = params.LANE|default("")|string %}
{% set spool_id = params.SPOOL_ID|default(0)|int %}
# Track first, in case AFC's real implementation (called below) ever
# errors or aborts partway -- we'd rather have the tracking recorded
# even if something downstream goes wrong, since SET_SPOOL_ID itself
# is what actually assigns the spool in AFC/Spoolman; our tracking is
# just bookkeeping on top of that, not a precondition for it.
{% if lane != "" %}
_PA_TRACK_LANE_SPOOL LANE={lane} SPOOL_ID={spool_id}
{% endif %}
# Call AFC's real implementation
_PA_SET_SPOOL_ID_BASE {rawparams}
[gcode_macro _PA_TRACK_LANE_SPOOL]
description: Records lane->spool_id for PA caching. Call right after SET_SPOOL_ID.
# LANE=<lane_name> SPOOL_ID=<int>
gcode:
{% set lane = params.LANE|default("")|string %}
{% set spool_id = params.SPOOL_ID|default(0)|int %}
{% if lane != "" %}
SAVE_VARIABLE VARIABLE=pa_lane_{lane}_spool_id VALUE={spool_id}
{action_respond_info("Tracked lane %s -> spool_id %d" % (lane, spool_id))}
{% else %}
{action_raise_error("_PA_TRACK_LANE_SPOOL: LANE parameter required")}
{% endif %}
# ---------------------------------------------------------------------------
# PA cache, keyed by spool_id, stored via Klipper's own save_variables.
# Requires a [save_variables] section in printer.cfg (see header notes).
#
# IMPORTANT: printer.save_variables.variables is read DIRECTLY inline in
# each macro below (not via a helper sub-macro + SET_GCODE_VARIABLE
# round-trip). This matters because Klipper's Jinja2 templates are fully
# evaluated up-front when a macro is triggered -- if macro A calls macro
# B to set a value and then macro A's own template (in the SAME trigger)
# tries to read that value back, the read happens at template-render
# time, BEFORE B has actually executed. So a pattern like:
# _PA_CACHE_GET SPOOL_ID={spool_id}
# {% set cache = printer["gcode_macro _PA_CACHE_RESULT"] %}
# would silently read stale data. Reading printer.save_variables.variables
# directly avoids this, since it is a live printer status object, not a
# value set earlier in the same template render.
# ---------------------------------------------------------------------------
[gcode_macro _PA_CACHE_SET]
description: Internal -- stores a PA value for a spool_id
# SPOOL_ID=<int> PA=<float>
gcode:
{% set spool_id = params.SPOOL_ID|default(0)|int %}
{% set pa = params.PA|default(0.0)|float %}
{% set varname = "pa_value_spool_" ~ spool_id %}
SAVE_VARIABLE VARIABLE={varname} VALUE={pa}
# ---------------------------------------------------------------------------
# Core: calibrate (or restore from cache) PA for one lane
# ---------------------------------------------------------------------------
[gcode_macro LANE_CALIBRATE_PA]
description: Calibrate PA for one AFC lane using bd_pressure, with caching
# LANE=<lane_name> [FORCE=1]
gcode:
{% set lane = params.LANE|default("")|string %}
{% set force = params.FORCE|default(0)|int %}
{% set cfg = printer["gcode_macro _PA_CFG"] %}
{% set vars = printer.save_variables.variables %}
{% if lane == "" %}
{action_raise_error("LANE_CALIBRATE_PA: LANE parameter required")}
{% endif %}
{% set lane_varname = "pa_lane_" ~ lane ~ "_spool_id" %}
{% set spool_id = vars[lane_varname]|default(0)|int if lane_varname in vars else 0 %}
{% if spool_id == 0 %}
{action_respond_info("LANE_CALIBRATE_PA: no spool_id recorded for %s yet. Run SET_SPOOL_ID LANE=%s SPOOL_ID=<id> then _PA_TRACK_LANE_SPOOL LANE=%s SPOOL_ID=<id>." % (lane, lane, lane))}
{% else %}
{% set pa_varname = "pa_value_spool_" ~ spool_id %}
{% set cache_found = pa_varname in vars %}
{% set cached_pa = vars[pa_varname]|float if cache_found else 0.0 %}
{% if cache_found and force == 0 %}
{action_respond_info("Using cached PA=%.4f for %s (spool %d)" % (cached_pa, lane, spool_id))}
SET_PRESSURE_ADVANCE ADVANCE={cached_pa}
{% else %}
CHANGE_TOOL LANE={lane}
# Nozzle temp: use the extruder's current target, which AFC
# already sets from Spoolman when the lane was loaded. Fall
# back to the configured default if nothing is set yet.
{% set target = printer.extruder.target|default(0)|float %}
{% set nozzle_temp = target if target > 0 else cfg.fallback_nozzle_temp|float %}
{action_respond_info("Calibrating PA for %s (spool %d) at %.0fC..." % (lane, spool_id, nozzle_temp))}
M117 Calibrating PA: {lane}
G1 Z50 F3000
G4 P400
G1 X30 Y349 Z10 F3000
G4 P400
G1 Z0 F3000
G4 P400
PA_CALIBRATE NOZZLE_TEMP={nozzle_temp} MAX_VOLUMETRIC={cfg.max_volumetric} ACC_WALL={cfg.acc_wall} TRAVEL_SPEED={cfg.travel_speed} ACC_TO_DECEL_FACTOR={cfg.acc_to_decel_factor}
# IMPORTANT: we do NOT read printer.extruder.pressure_advance
# here. Klipper fully evaluates a macro's Jinja2 template
# up-front, before any of the resulting gcode commands
# (including PA_CALIBRATE above) actually execute. So a
# {% set new_pa = printer.extruder.pressure_advance %} line
# placed here would capture the OLD value, from before
# calibration ran, not the new one.
# Source: https://www.klipper3d.org/Command_Templates.html
# ("Macros are first evaluated in entirety and only then are
# the resulting commands executed.")
#
# Instead we call a SEPARATE macro as a plain gcode command.
# Klipper queues and executes gcode commands strictly in
# order, each fully completing before the next one is even
# evaluated -- so by the time _PA_AFTER_CALIBRATE is
# triggered (and its own template is rendered), PA_CALIBRATE
# above has already finished and printer.extruder.pressure_advance
# is current.
_PA_AFTER_CALIBRATE LANE={lane} SPOOL_ID={spool_id} CACHE_FOUND={1 if cache_found else 0} CACHED_PA={cached_pa}
BLOBIFIER_CLEAN
G4 P400
G1 Z10 F3000
G4 P400
{% endif %}
{% endif %}
[gcode_macro _PA_AFTER_CALIBRATE]
description: Internal -- runs immediately after PA_CALIBRATE completes; reads the fresh PA value and caches it
# LANE=<lane_name> SPOOL_ID=<int> CACHE_FOUND=<0|1> CACHED_PA=<float>
gcode:
{% set lane = params.LANE|default("")|string %}
{% set spool_id = params.SPOOL_ID|default(0)|int %}
{% set cache_found = params.CACHE_FOUND|default(0)|int %}
{% set cached_pa = params.CACHED_PA|default(0.0)|float %}
{% set cfg = printer["gcode_macro _PA_CFG"] %}
# This template is evaluated only once this macro is actually
# triggered -- which happens after PA_CALIBRATE (queued earlier by
# the caller) has fully completed. So this read is fresh and correct.
{% set new_pa = printer.extruder.pressure_advance|default(0.0)|float %}
{action_respond_info("Calibration complete for %s: PA=%.4f" % (lane, new_pa))}
M117 PA cal done: {new_pa|round(4)}
{% set delta = (new_pa - cached_pa)|abs if cache_found else 999 %}
{% if delta > cfg.write_threshold|float %}
_PA_CACHE_SET SPOOL_ID={spool_id} PA={new_pa}
{action_respond_info("Writing PA=%.4f to Spoolman spool %d" % (new_pa, spool_id))}
RUN_SHELL_COMMAND CMD=_pa_write_spoolman PARAMS="{spool_id} {cfg.spoolman_url} {cfg.spoolman_field} {new_pa}"
{% else %}
{action_respond_info("PA change below threshold (%.4f), skipping cache/Spoolman write" % cfg.write_threshold|float)}
{% endif %}
# ---------------------------------------------------------------------------
# Calibrate every lane in a comma-separated list (skips lanes with no
# spool_id recorded yet). Call this from PRINT_START.
# ---------------------------------------------------------------------------
[gcode_macro PA_CALIBRATE_ALL_LANES]
description: Calibrate (or restore) PA for all listed AFC lanes
# LANES="lane1,lane2,..." [FORCE=1]
# NOTE: the toolhead switch into each lane (CHANGE_TOOL LANE={lane}) now
# lives INSIDE LANE_CALIBRATE_PA itself, on the cache-miss path only --
# not here. This avoids a double tool-change per lane (one here, one
# inside LANE_CALIBRATE_PA) and means a cache HIT for a given lane skips
# the physical tool change entirely, since SET_PRESSURE_ADVANCE alone
# doesn't require the lane to actually be loaded in the toolhead.
# If you need every lane in the list physically staged/loaded regardless
# of cache state (e.g. as a pre-print warm-up), call CHANGE_TOOL LANE=
# yourself for each lane, or use FORCE=1 here to force every lane
# through the cache-miss (and therefore CHANGE_TOOL) path.
# UNVERIFIED, NEEDS LIVE TESTING:
# - CHANGE_TOOL LANE={lane} when {lane} is ALREADY the loaded lane --
# confirm it doesn't error or do anything unexpected in that case.
# - On a cache-miss run across all 5 lanes, that's up to 5 full tool
# changes plus 5 calibrations back to back. Confirm timing/wear is
# acceptable for how often you'll actually call this.
gcode:
{% set raw_lanes = params.LANES|default(printer["gcode_macro _PA_CFG"].lane_names)|string %}
{% set force = params.FORCE|default(0)|int %}
{% set lane_list = raw_lanes.split(",") %}
{action_respond_info("PA_CALIBRATE_ALL_LANES starting for: %s" % raw_lanes)}
{% for lane in lane_list %}
{% set lane = lane.strip() %}
{% if lane != "" %}
LANE_CALIBRATE_PA LANE={lane} FORCE={force}
{% endif %}
{% endfor %}
{action_respond_info("PA_CALIBRATE_ALL_LANES complete.")}
# ---------------------------------------------------------------------------
# Tool-change detection: polling, not interception. See "WHY POLLING
# INSTEAD OF INTERCEPTING A TOOL-CHANGE COMMAND" near the top of this
# file for the full reasoning and the three approaches that were tried
# and ruled out first.
#
# Short version: printer.AFC.current_load (confirmed in AFC.py's
# get_status(): str['current_load'] = self.current) always reflects the
# currently-loaded lane regardless of how it got there, so polling it
# avoids any dependency on which specific command (T0-T4, CHANGE_TOOL,
# BT_CHANGE_TOOL, runout remapping) triggered the change.
# ---------------------------------------------------------------------------
[delayed_gcode _PA_POLL_LANE_CHANGE]
# delayed_gcode sections only support 'gcode' and 'initial_duration' --
# 'description' is not a valid option here (it's a gcode_macro-only
# field), so the explanation lives in this comment instead:
# Internal -- polls printer.AFC.current_load and applies cached PA when it changes.
initial_duration: 2
gcode:
{% set vars = printer.save_variables.variables %}
{% set last_seen = vars["pa_last_seen_lane"]|default("")|string if "pa_last_seen_lane" in vars else "" %}
# Guard against printer.AFC not existing at all (e.g. AFC failed to
# load, or this printer uses a differently-named AFC config section).
# Without this guard, a missing printer.AFC would make this whole
# delayed_gcode error out every 2 seconds, forever, which is worse
# than just skipping this poll cycle.
{% set afc_exists = "AFC" in printer %}
{% set current = printer.AFC.current_load|default(none) if afc_exists else none %}
{% set current_str = current|string if current is not none else "" %}
{% if not afc_exists %}
{% if last_seen != "__AFC_MISSING__" %}
SAVE_VARIABLE VARIABLE=pa_last_seen_lane VALUE='"__AFC_MISSING__"'
{action_respond_info("_PA_POLL_LANE_CHANGE: printer.AFC not found -- AFC may not be loaded. PA auto-apply on tool change is disabled until this is resolved.")}
{% endif %}
{% elif current_str != last_seen %}
SAVE_VARIABLE VARIABLE=pa_last_seen_lane VALUE='"{current_str}"'
{% if current_str != "" %}
_PA_APPLY_FOR_LANE LANE={current_str}
{% endif %}
{% endif %}
# Re-arm: check again in 2 seconds. This keeps running indefinitely
# once Klipper starts (initial_duration above triggers the first run).
# Re-arms even when printer.AFC is missing, so if AFC loads later
# (e.g. after a manual RESTART) this starts working without needing
# its own restart.
UPDATE_DELAYED_GCODE ID=_PA_POLL_LANE_CHANGE DURATION=2
[gcode_macro _PA_APPLY_FOR_LANE]
description: Internal -- applies cached PA for a lane without calibrating
# LANE=<lane_name>
gcode:
{% set lane = params.LANE|default("")|string %}
{% set vars = printer.save_variables.variables %}
{% set lane_varname = "pa_lane_" ~ lane ~ "_spool_id" %}
{% set spool_id = vars[lane_varname]|default(0)|int if lane_varname in vars else 0 %}
{% if spool_id > 0 %}
{% set pa_varname = "pa_value_spool_" ~ spool_id %}
{% if pa_varname in vars %}
{% set cached_pa = vars[pa_varname]|float %}
SET_PRESSURE_ADVANCE ADVANCE={cached_pa}
{action_respond_info("Lane %s loaded: applied cached PA=%.4f (spool %d)" % (lane, cached_pa, spool_id))}
M117 PA: {lane} = {cached_pa|round(4)}
{% else %}
{action_respond_info("Lane %s loaded: spool %d has no cached PA yet. Run LANE_CALIBRATE_PA LANE=%s or PA_CALIBRATE_ALL_LANES." % (lane, spool_id, lane))}
{% endif %}
{% else %}
{action_respond_info("Lane %s loaded: no spool_id recorded yet (run SET_SPOOL_ID first)." % lane)}
{% endif %}
# ---------------------------------------------------------------------------
# Force re-calibration of one lane (e.g. after a filament change you
# suspect needs fresh tuning) and update the cache.
# ---------------------------------------------------------------------------
[gcode_macro LANE_RECAL_PA]
description: Force re-calibrate PA for one lane and update the cache
# LANE=<lane_name>
gcode:
{% set lane = params.LANE|default("lane1")|string %}
LANE_CALIBRATE_PA LANE={lane} FORCE=1
# ---------------------------------------------------------------------------
# Status helper
# ---------------------------------------------------------------------------
[gcode_macro PA_STATUS]
description: Show current PA cache and lane->spool_id mappings
gcode:
{% set cfg = printer["gcode_macro _PA_CFG"] %}
{% set lane_list = cfg.lane_names.split(",") %}
{% set vars = printer.save_variables.variables %}
{action_respond_info("=== PA Status ===")}
{% for lane in lane_list %}
{% set lane = lane.strip() %}
{% if lane != "" %}
{% set spool_varname = "pa_lane_" ~ lane ~ "_spool_id" %}
{% set spool_id = vars[spool_varname]|default(0)|int if spool_varname in vars else 0 %}
{% if spool_id > 0 %}
{% set pa_varname = "pa_value_spool_" ~ spool_id %}
{% if pa_varname in vars %}
{action_respond_info(" %s (spool %d): PA=%.4f cached" % (lane, spool_id, vars[pa_varname]|float))}
{% else %}
{action_respond_info(" %s (spool %d): not yet calibrated" % (lane, spool_id))}
{% endif %}
{% else %}
{action_respond_info(" %s: no spool_id recorded" % lane)}
{% endif %}
{% endif %}
{% endfor %}
{action_respond_info("Current extruder PA: %.4f" % printer.extruder.pressure_advance|default(0)|float)}
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
BASE_URL=$1
FIELD=$2
curl -sf -X POST "${BASE_URL}/api/v1/field/spool/${FIELD}" \
-H "Content-Type: application/json" \
-d "{\"name\": \"Pressure Advance\", \"field_type\": \"float\", \"default_value\": \"0\"}" \
> /tmp/pa_setup_result 2>&1
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
SPOOL_ID=$1
BASE_URL=$2
FIELD=$3
PA_VALUE=$4
CURRENT=$(curl -sf "${BASE_URL}/api/v1/spool/${SPOOL_ID}" 2>/dev/null)
EXTRA=$(echo "$CURRENT" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
e = d.get('extra', {})
e['$FIELD'] = '$PA_VALUE'
print(json.dumps(e))
except Exception:
print(json.dumps({'$FIELD': '$PA_VALUE'}))
" 2>/dev/null)
if [ -z "$EXTRA" ]; then
EXTRA="{\"$FIELD\": \"$PA_VALUE\"}"
fi
curl -sf -X PATCH "${BASE_URL}/api/v1/spool/${SPOOL_ID}" \
-H "Content-Type: application/json" \
-d "{\"extra\": $EXTRA}" > /tmp/pa_spoolman_write_result 2>&1
+171
View File
@@ -0,0 +1,171 @@
############### Config options ##################
[gcode_macro _BEDFANVARS]
variable_threshold: 50 # If bed temp target is above this threshold, fans will be enabled. If temp is set to below this threshold, fans will be disabled.
variable_fast: 1.0 # Fan speed once bed temp is reached
variable_slow: 0.5 # Fan speed while bed is heating
variable_cooldown_temp: 40 # NEW: fans stop below this temp
gcode:
########## Aliases #########
[gcode_macro BEDFANSSLOW]
gcode:
# Vars
{% set SLOW = printer["gcode_macro _BEDFANVARS"].slow|float %}
SET_FAN_SPEED FAN=BedFans SPEED={SLOW}
[gcode_macro BEDFANSFAST]
gcode:
# Vars
{% set FAST = printer["gcode_macro _BEDFANVARS"].fast|float %}
SET_FAN_SPEED FAN=BedFans SPEED={FAST}
[gcode_macro BEDFANSOFF]
gcode:
SET_FAN_SPEED FAN=BedFans SPEED=0
############ Command overrides ############
# Override, set fan speeds to low and start monitoring loop.
[gcode_macro SET_HEATER_TEMPERATURE]
rename_existing: _SET_HEATER_TEMPERATURE
gcode:
# Parameters
{% set HEATER = params.HEATER|default("None") %}
{% set TARGET = params.TARGET|default(0)|int %}
# Vars
{% set THRESHOLD = printer["gcode_macro _BEDFANVARS"].threshold|int %}
{% set COOLDOWN = printer["gcode_macro _BEDFANVARS"].cooldown_temp|int %}
{% if HEATER|lower == "extruder" %}
M104 S{TARGET}
# {% elif HEATER|lower == "iHeater_H" %}
# _SET_HEATER_TEMPERATURE {rawparams}
{% elif HEATER|lower == "heater_bed" %}
M99140 S{TARGET}
# Always cancel cooldown loop first (prevents overlap)
UPDATE_DELAYED_GCODE ID=bedfancooldown DURATION=0
# --- NEW LOGIC BLOCK ---
{% if TARGET == 0 %}
# Bed turning off → start cooldown behavior
{% if printer.heater_bed.temperature|int > COOLDOWN %}
BEDFANSSLOW
UPDATE_DELAYED_GCODE ID=bedfancooldown DURATION=5
{% else %}
BEDFANSOFF
{% endif %}
# Stop normal heating loop if running
UPDATE_DELAYED_GCODE ID=bedfanloop DURATION=0
{% elif TARGET >= THRESHOLD %}
# Heating above threshold
BEDFANSSLOW
UPDATE_DELAYED_GCODE ID=bedfanloop DURATION=1
{% else %}
# Low temp request
BEDFANSOFF
UPDATE_DELAYED_GCODE ID=bedfanloop DURATION=0
{% endif %}
# --- END NEW LOGIC ---
{% else %}
# {action_respond_info("(Bedfans macro)Heater %s not supported (trying classic _SET_HEATER_TEMPERATURE)" % HEATER)}
_SET_HEATER_TEMPERATURE {rawparams}
{% endif %}
# Override M190 (Wait for Bed Temperature)
# As a bonus, use TEMPERATURE_WAIT so we don't have to wait for PID to level off.
[gcode_macro M190]
rename_existing: M99190
gcode:
# Parameters
{% set S = params.S|int %}
# Vars
{% set THRESHOLD = printer["gcode_macro _BEDFANVARS"].threshold|int %}
{% if S >= THRESHOLD %}
BEDFANSSLOW # >= Threshold temp: Low speed fans while heating
{% else %}
BEDFANSOFF # < Threshold temp: Turn bed fans off
{% endif %}
M140 {% for p in params
%}{'%s%s' % (p, params[p])}{%
endfor %} # Set bed temp
{% if S != 0 %}
TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={S|int} MAXIMUM={S|int + 5} # Wait for bed temp within 5 degrees
{% endif %}
# Post-heating fan speeds
{% if S >= THRESHOLD %}
BEDFANSFAST # >= Threshold temp: Higher speed fans after heating finished
{% endif %}
# Replace M140 (Set Bed Temperature) to just be an alias of SET_HEATER_TEMPERATURE (which has associated bed fan logic if enabled)
[gcode_macro M140]
rename_existing: M99140
gcode:
# Parameters
{% set S = params.S|float %}
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={S}
# Replace TURN_OFF_HEATERS
# [gcode_macro TURN_OFF_HEATERS]
# rename_existing: _TURN_OFF_HEATERS
# gcode:
# BEDFANSOFF
# _TURN_OFF_HEATERS
[gcode_macro TURN_OFF_HEATERS]
rename_existing: _TURN_OFF_HEATERS
gcode:
# Vars
{% set COOLDOWN = printer["gcode_macro _BEDFANVARS"].cooldown_temp|int %}
# Only keep fans running if bed is still hot
{% if printer.heater_bed.temperature|int > COOLDOWN %}
BEDFANSSLOW
UPDATE_DELAYED_GCODE ID=bedfancooldown DURATION=5
{% else %}
BEDFANSOFF
{% endif %}
_TURN_OFF_HEATERS
################ Monitoring loop #####################
# Turns bed fans to "fast" speed once target bed temp is reached.
[delayed_gcode bedfanloop]
gcode:
# Vars
{% set THRESHOLD = printer["gcode_macro _BEDFANVARS"].threshold|int %}
{% if printer.heater_bed.target >= THRESHOLD %} # Continue only if target temp greater than threshold.
{% if printer.heater_bed.temperature|int >= (printer.heater_bed.target|int - 1) %}
BEDFANSFAST # If within 1 degree of target temp: Higher speed fans
{% else %}
UPDATE_DELAYED_GCODE ID=bedfanloop DURATION=5 # If temp not reached yet: loop again
{% endif %}
{% endif %}
[delayed_gcode bedfancooldown]
gcode:
# Vars
{% set COOLDOWN = printer["gcode_macro _BEDFANVARS"].cooldown_temp|int %}
{% if printer.heater_bed.temperature|int > COOLDOWN %}
BEDFANSSLOW
UPDATE_DELAYED_GCODE ID=bedfancooldown DURATION=10
{% else %}
BEDFANSOFF
{% endif %}
+300
View File
@@ -0,0 +1,300 @@
###########################################################
# Filament Drying #
###########################################################
# #
# Macro Settings #
# Z-axis bed movement - value in mm #
# Chamber with chamber heater - value in degrees Celsius #
# Bed - value in degrees Celsius #
# Drying duration - value in minutes #
# Recirculation fan - value in % #
# Recirculation runtime - value in minutes #
# Recirculation wait time - value in minutes #
# Exhaust fan - value in % #
# Exhaust runtime - value in minutes #
# Exhaust wait time - value in minutes #
# Setting run and wait times to "0" will keep #
# the corresponding fan running continuously #
# #
###########################################################
[gcode_macro START_DRYING]
description: Start the filament dryer for the heated bed.
gcode:
# Drying parameters
{% set MaxChamberTemperature = 60.0 %}
{% set MaxBedTemperature = 100.0 %}
BEDFANSFAST
# Check the chamber and bed temperatures
{% set Chamber = params.CHAMBER|default(25.0)|float %}
{% set Bed = params.BED|default(45.0)|float %}
{% if Chamber < 0 %}
M118 Error: Chamber temperature cannot be negative.
M117 Error: Chamber temperature negative.
{% set SkipRemainingCommands = True %}
{% elif Chamber > MaxChamberTemperature %}
M118 Warning: Chamber temperature exceeds the maximum value of { MaxChamberTemperature }°C. Drying will be stopped.
M117 Warning: Chamber temperature exceeds the maximum value of { MaxChamberTemperature }°C. Drying will be stopped.
{% set SkipRemainingCommands = True %}
{% else %}
{% set SkipRemainingCommands = False %}
{% endif %}
{% if not SkipRemainingCommands %}
{% if Bed < 0 %}
M118 Error: Bed temperature cannot be negative.
M117 Error: Bed temperature negative.
{% set SkipRemainingCommands = True %}
{% elif Bed == 0 %}
M118 Error: Bed temperature is 0 degrees. How will drying be achieved?
M117 Error: Bed temperature is 0 degrees. How will drying be achieved?
{% set SkipRemainingCommands = True %}
{% elif Bed > MaxBedTemperature %}
M118 Warning: Bed temperature exceeds the maximum value of { MaxBedTemperature }°C. Drying will be stopped.
M117 Warning: Bed temperature exceeds the maximum value of { MaxBedTemperature }°C. Drying will be stopped.
{% set SkipRemainingCommands = True %}
{% else %}
{% set SkipRemainingCommands = False %}
{% endif %}
{% endif %}
# Check exhaust and recirculation fan values
{% set Exhaust = params.EXHAUST|default(100)|int %}
{% set Recirculation = params.RECIRCULATION|default(100)|int %}
{% if Exhaust < 0 %}
M118 Error: Exhaust value cannot be negative.
M117 Error: Exhaust value negative.
{% set SkipRemainingCommands = True %}
{% elif Recirculation < 0 %}
M118 Error: Recirculation value cannot be negative.
M117 Error: Recirculation value negative.
{% set SkipRemainingCommands = True %}
{% elif Exhaust > 100 %}
M118 Warning: Exhaust value exceeds the maximum value of 100%. Value will be limited to 100%.
M117 Warning: Exhaust value exceeds the maximum value of 100%. Value will be limited to 100%.
{% set Exhaust = 100 %}
{% elif Recirculation > 100 %}
M118 Warning: Recirculation value exceeds the maximum value of 100%. Value will be limited to 100%.
M117 Warning: Recirculation value exceeds the maximum value of 100%. Value will be limited to 100%.
{% set Recirculation = 100 %}
{% endif %}
{% if not SkipRemainingCommands %}
; Enter drying duration in minutes, limited to a maximum of 12 hours (720 minutes)
{% set DryingDurationMinutes = params.DRYING_DURATION|default(240)|int %}
{% if DryingDurationMinutes > 720 %}
{% set DryingDurationMinutes = 720 %}
M118 Warning: Drying duration exceeds 12 hours. Duration limited to 12 hours.
M117 Warning: Drying duration exceeds 12 hours. Duration limited to 12 hours.
{% endif %}
{% set DryingDuration = DryingDurationMinutes * 60 %}
# Recirculation and exhaust fan settings
{% set WaitTime_ExhaustMinutes = params.WAIT_TIME_EXHAUST|default(60)|int %}
{% set Runtime_ExhaustMinutes = params.RUNTIME_EXHAUST|default(30)|int %}
{% set WaitTime_RecirculationMinutes = params.WAIT_TIME_RECIRCULATION|default(60)|int %}
{% set Runtime_RecirculationMinutes = params.RUNTIME_RECIRCULATION|default(30)|int %}
# Check wait and runtime settings
{% if WaitTime_ExhaustMinutes < 0 or Runtime_ExhaustMinutes < 0 or WaitTime_RecirculationMinutes < 0 or Runtime_RecirculationMinutes < 0 %}
M118 Error: Wait and runtime settings cannot be negative.
M117 Error: One or more wait or runtime settings are negative.
{% set SkipRemainingCommands = True %}
{% endif %}
{% if not SkipRemainingCommands %}
# Convert wait and runtime to seconds
{% set WaitTime_Exhaust = WaitTime_ExhaustMinutes * 60 %}
{% set Runtime_Exhaust = Runtime_ExhaustMinutes * 60 %}
{% set WaitTime_Recirculation = WaitTime_RecirculationMinutes * 60 %}
{% set Runtime_Recirculation = Runtime_RecirculationMinutes * 60 %}
# Convert percentage to PWM value (0-255)
{% set ExhaustPWM = (Exhaust * 255 / 100)|int %}
{% set RecirculationPWM = (Recirculation * 255 / 100)|int %}
# Ensure PWM values are within the range (0-255)
{% if ExhaustPWM > 255 %}
{% set ExhaustPWM = 255 %}
{% elif ExhaustPWM < 0 %}
{% set ExhaustPWM = 0 %}
{% endif %}
{% if RecirculationPWM > 255 %}
{% set RecirculationPWM = 255 %}
{% elif RecirculationPWM < 0 %}
{% set RecirculationPWM = 0 %}
{% endif %}
# Issue fan speed warnings if speed is 0
{% if Exhaust == 0 and (WaitTime_Exhaust > 0 or Runtime_Exhaust > 0) %}
M118 Warning: Exhaust fan speed is 0, but wait time or runtime specified.
M117 Warning: Exhaust fan speed is 0, but wait time or runtime specified.
{% endif %}
{% if Recirculation == 0 and (WaitTime_Recirculation > 0 or Runtime_Recirculation > 0) %}
M118 Warning: Recirculation fan speed is 0, but wait time or runtime specified.
M117 Warning: Recirculation fan speed is 0, but wait time or runtime specified.
{% endif %}
# Check if fans should run continuously
{% set ExhaustPermanent = (WaitTime_Exhaust == 0 and Runtime_Exhaust == 0) %}
{% set RecirculationPermanent = (WaitTime_Recirculation == 0 and Runtime_Recirculation == 0) %}
# Check homing status of the toolhead
{% if printer.toolhead.homed_axes|default("") != "xyz" %}
M118 Printer is homing!
M117 Printer is homing!
G28 ; Home all axes
M118 Homing completed
M117 Homing completed
{% else %}
M118 Homing already completed!
M117 Homing already completed!
{% endif %}
G1 X175 Y175 F3000 ; Move the toolhead quickly to X=1, Y=1
# Z-axis bed movement
{% set Z_movement = params.Z_MOVEMENT|default(250.0)|float %}
{% if Z_movement > 0 %}
G1 Z+{Z_movement} F1500 ; Move the bed up in the Z-axis at the specified speed
G1 X175 Y350 Z+{Z_movement} F1500
M118 Bed moved by Z={Z_movement}
M117 Bed moved by Z={Z_movement}
{% endif %}
# Wait until the Z-displacement is complete
M118 Wait for completion of Z-displacement...
M117 Wait for completion of Z-displacement...
# Repeat M114 to ensure that the Z position is reached
M114 # Query the current position
G4 P1000 # Short pause to query the Z position
# When Z-movement is complete, start countdown
M118 Z-displacement completed.
M117 Z-displacement completed.
# Wait 30 seconds
M118 Please insert filament and close the door! Drying starts in 30 seconds.
M117 Please insert filament and close the door! Drying starts in 30 seconds.
G4 P5000 # Wait 5 seconds
# Loop for the countdown from 25 seconds to 5 seconds
{% for i in range(1, 6) %}
M118 Drying in {25 - 5 * (i - 1)} seconds!
M117 Drying in {25 - 5 * (i - 1)} seconds!
G4 P5000 # wait 5 seconds
{% endfor %}
# Turn on heating elements
M140 S{ Bed } ; Set bed temperature without waiting.
M141 S{ Chamber } ; [OPTIONAL] Set chamber temperature.
# Set dryer status and start timer
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=time_remaining VALUE={ DryingDuration }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=bed_temperature VALUE={ Bed }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=chamber_temperature VALUE={ Chamber }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=exhaust VALUE={ Exhaust }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=exhaust_pwm VALUE={ ExhaustPWM }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=recirculation VALUE={ Recirculation }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=recirculation_pwm VALUE={ RecirculationPWM }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=waittime_exhaust VALUE={ WaitTime_Exhaust }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=runtime_exhaust VALUE={ Runtime_Exhaust }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=waittime_recirculation VALUE={ WaitTime_Recirculation }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=runtime_recirculation VALUE={ Runtime_Recirculation }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=exhaust_permanent VALUE={ ExhaustPermanent }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=recirculation_permanent VALUE={ RecirculationPermanent }
# Start the delayed gcode for timer
UPDATE_DELAYED_GCODE ID=DRYING_TIMER DURATION=1
M118 Drying in progress...
M117 Drying in progress...
{% endif %}
{% endif %}
[gcode_macro STOP_DRYING]
description: Stops the filament drying process
gcode:
# Turn off heating elements and fans
M140 S0 ; Turn off heating element
M141 S0 ; [OPTIONAL] Turn off chamber heater/fan
M106 P2 S0 ; Turn off recirculation fan
M106 P3 S0 ; Turn off exhaust fan
# Stop timer and update status
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=time_remaining VALUE=0
UPDATE_DELAYED_GCODE ID=DRYING_TIMER DURATION=0
M117 Drying stopped! Please remove filament.
M118 Drying stopped! Please remove filament.
# Query print bed temperature
{% if printer.heater_bed.temperature > 40 %}
M117 Attention: Print bed above 40°C! Take care when removing the filament roll.
M118 Attention: Print bed above 40°C! Caution when removing the filament roll.
{% else %}
M117 Print bed temperature OK.
M118 Print bed temperature OK.
{% endif %}
# Send Telegram notification
#RESPOND PREFIX=tgnotify MSG="Filament drying has been stopped. Please remove the filament."
[gcode_macro STATUS_DRYING]
variable_time_remaining: 0
variable_bed_temperature: 0
variable_chamber_temperature: 0
variable_exhaust: 100
variable_exhaust_pwm: 255
variable_recirculation: 100
variable_recirculation_pwm: 255
variable_waittime_exhaust: 60
variable_runtime_exhaust: 30
variable_waittime_recirculation: 60
variable_runtime_recirculation: 30
variable_exhaust_permanent: False
variable_recirculation_permanent: False
gcode:
{% set cycle_time_exhaust = waittime_exhaust + runtime_exhaust %}
{% set cycle_time_recirculation = waittime_recirculation + runtime_recirculation %}
{% if time_remaining > 0 %}
{% set current_time = time_remaining - 1 %}
{% set hours = (current_time // 3600) %}
{% set minutes = (current_time % 3600) // 60 %}
{% set seconds = (current_time % 3600) % 60 %}
{% set time_display = "%02d:%02d:%02d" % (hours, minutes, seconds) %}
# Update fan states based on the current cycle
{% if exhaust_permanent or (current_time % cycle_time_exhaust) < runtime_exhaust %}
; Exhaust fan running
M106 P3 S{ exhaust_pwm }
{% else %}
; Exhaust fan off
M106 P3 S0
{% endif %}
{% if recirculation_permanent or (current_time % cycle_time_recirculation) < runtime_recirculation %}
# Recirculation fan running
M106 P2 S{ recirculation_pwm }
{% else %}
# Recirculation fan off
M106 P2 S0
{% endif %}
M117 Remaining time: { time_display }
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=time_remaining VALUE={ current_time }
{% else %}
# Stop drying when time runs out
STOP_DRYING
{% endif %}
[delayed_gcode DRYING_TIMER]
gcode:
UPDATE_DELAYED_GCODE ID=DRYING_TIMER DURATION=1
STATUS_DRYING
+301
View File
@@ -0,0 +1,301 @@
##
## HEAT_SOAK HEATER=<heater_name> TARGET=<target_heater_temperature> SOAKER=<heat_soak_temp_sensor_name> [RATE=<heat_soak_temp_rate_of_change>] [TIMEOUT=<time_to_abort_heat_soak>]
##
##
## e.g.
## HEAT_SOAK HEATER=heater_bed TARGET=100 SOAKER="temperature_sensor top_bed"
##
## Based on work by blalor: https://klipper.discourse.group/t/interruptible-heat-soak/1552
##
## Modified by me
##
[gcode_macro HEAT_SOAK]
description: Wait for a temperature sensor to reach thermal equilibrium
variable_last_temp: 0
variable_last_soak_temp_rate: None
variable_temp_history: None
variable_smoothed_temp_history: None
variable_stage: "done" ## heating -> soaking -> done -> None
variable_total_time_elapsed: 0
variable_soak_time_remaining: 30
variable_check_interval: 1
variable_heating_report_interval: 2
variable_soaking_report_interval: 5
variable_heater_sensor: None
variable_soaker_sensor: None
variable_target_temp: 0.0
variable_min_soak_temp: 0
variable_target_rate: 0.3
variable_complete: None
variable_cancel: None
variable_temp_smooth_time: 4.0
variable_rate_smooth_time: 20.0
variable_resume_trigger: False
variable_was_print_active: False
gcode:
{ action_respond_info( "Heat Soak starting" )}
# Soaker is required
{% set SOAKER = params.SOAKER | string %}
{% set RATE = params.RATE | default(0.3) | float %} ## in degrees C per minute
{% set SOAK_TEMP = params.SOAK_TEMP | default(0.0) | float %}
{% set HEATER = (params.HEATER | string) %} # Optional heater
{% set TARGET = params.TARGET | default(0) | float %}
{% set TIMEOUT = (params.TIMEOUT | default(30) | int) * 60 %} ## minutes to seconds
{% set TEMP_SMOOTH = params.TEMP_SMOOTH | default(4.0) | float %} ## seconds for temp smoothing
{% set RATE_SMOOTH = params.RATE_SMOOTH | default(20) | float %} ## seconds for rate smoothing
{% set COMPLETE = (params.COMPLETE | string) %}
{% set CANCEL = (params.CANCEL | string) %}
{% set HEATING_REPORT_INTERVAL = params.HEATING_REPORT_INTERVAL | default(2) | int %}
{% set SOAKING_REPORT_INTERVAL = params.SOAKING_REPORT_INTERVAL | default(5) | int %}
# User writeable variables
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=heater_sensor VALUE="{HEATER | pprint}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_temp VALUE={TARGET}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=min_soak_temp VALUE={SOAK_TEMP}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soaker_sensor VALUE="{SOAKER | pprint}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_rate VALUE={RATE}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={TIMEOUT}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=complete VALUE="'{COMPLETE}'"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=cancel VALUE="'{CANCEL}'"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=temp_smooth_time VALUE={TEMP_SMOOTH}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=rate_smooth_time VALUE={RATE_SMOOTH}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=heating_report_interval VALUE={HEATING_REPORT_INTERVAL}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soaking_report_interval VALUE={SOAKING_REPORT_INTERVAL}
# Internal variables
{% set soak_temp = printer[SOAKER].temperature %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=last_temp VALUE={soak_temp}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=temp_history VALUE="{[soak_temp] | pprint}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=smoothed_temp_history VALUE="{[] | pprint}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE=0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=last_soak_temp_rate VALUE=None
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=check_interval VALUE=1.0
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=resume_trigger VALUE=False
{% set is_print_active = printer['virtual_sdcard'].is_active or printer['virtual_sdcard'].file_position != 0.0 %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=was_print_active VALUE={is_print_active}
# start optional heater
{% if (HEATER and TARGET != 0.0) %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'heating'"
SET_HEATER_TEMPERATURE HEATER={HEATER} TARGET={TARGET}
{% else %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'soaking'"
{% endif %}
# pause the print, if active
{% if is_print_active %}
PAUSE_BASE
{% endif %}
UPDATE_DELAYED_GCODE ID=_heat_soaker DURATION={check_interval}
[gcode_macro STOP_HEAT_SOAK]
description: stops heat soak activity without running any callbacks
gcode:
UPDATE_DELAYED_GCODE ID=_heat_soaker DURATION=0 # cancel any pending run
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'done'"
[gcode_macro CANCEL_HEAT_SOAK]
description: cancels an in-progress HEAT_SOAK cycle and runs the cancel callback
gcode:
# check that the HEAT_SOAK macro is active before canceling
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
{% set stage = heat_soak.stage %}
{% if stage in ("heating", "soaking") %}
STOP_HEAT_SOAK
{% if heat_soak.cancel %}
{heat_soak.cancel}
{% endif %}
{% endif %}
[gcode_macro HEAT_SOAK_RESUME]
description: Resume while heat soaking results in the soaking phase being skipped, any complete callback is run
gcode:
# {% set ON_RESUME = (params.ON_RESUME | default("_HEAT_SOAK__BASE_RESUME")) | string %}
{% set ON_RESUME = (params.ON_RESUME | default("_HEAT_SOAK__BASE_RESUME")) | string %}
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
{% set stage = heat_soak.stage %}
{% if stage == "heating" %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=resume_trigger VALUE=True
{% elif stage == "soaking" %}
STOP_HEAT_SOAK
{% if heat_soak.complete %}
{heat_soak.complete}
{% endif %}
{ON_RESUME} {% for p in params %}{'%s=%s ' % (p, params[p])}{% endfor %}
{% else %}
{ON_RESUME} {% for p in params %}{'%s=%s ' % (p, params[p])}{% endfor %}
{% endif %}
[gcode_macro RESUME]
description: Resume while heat soaking results in the soaking phase being skipped, any complete callback is run
rename_existing: _HEAT_SOAK__BASE_RESUME
gcode:
HEAT_SOAK_RESUME ON_RESUME=_HEAT_SOAK__BASE_RESUME
[delayed_gcode _heat_soaker]
# description: internal macro to handle heat soaking activity
gcode:
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
# debug
#{ action_respond_info( printer['gcode_macro HEAT_SOAK'] | tojson )}
# shut down cleanly if we detect that the user canceled or started a print while heat soaking
{% set file_position_is_zero = (printer['virtual_sdcard'].file_position == 0.0 )%}
{% if heat_soak.was_print_active and file_position_is_zero %}
STOP_HEAT_SOAK
{action_respond_info("HEAT_SOAK aborted. Detected a change in virtual_sdcard print state. Call STOP_HEAT_SOAK from CANCEL_PRINT to stop seeing this error message.")}
{% endif %}
# shut down cleanly if we detect that the user resumed the print while heat soaking
{% if not file_position_is_zero and not printer['pause_resume'].is_paused %}
STOP_HEAT_SOAK
{action_respond_info("HEAT_SOAK aborted. Print is no longer paused. Call STOP_HEAT_SOAK or HEAT_SOAK_RESUME from your RESUME macro to stop seeing this error message.")}
{% endif %}
# update total time elapsed
{% set total_time_elapsed = heat_soak.total_time_elapsed + heat_soak.check_interval %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE={ total_time_elapsed }
{% set soaker_sensor = printer[heat_soak.soaker_sensor] %}
{% set soak_temp = soaker_sensor.temperature %}
{% if heat_soak.heater_sensor %}
{% set heater_sensor = printer[heat_soak.heater_sensor] %}
{% set heater_temp = heater_sensor.temperature | round(1) %}
{% endif %}
{% set stage = heat_soak.stage %}
# compute the new temp difference and save history
{% set temp_history = heat_soak.temp_history %}
{% set smoothed_temp_history = heat_soak.smoothed_temp_history %}
{% set temp_diff = soak_temp - (temp_history | last) %}
{% set _ = temp_history.append(soak_temp) %}
{% set soak_temp_smoothed = None %}
{% set slope = None %}
{% if (temp_history | length) > heat_soak.temp_smooth_time %}
{% set temp_history = temp_history[1:] %}
# compute average temp
{% set soak_temp_smoothed = ((temp_history | sum) / (temp_history | length)) %}
# save raw value to history
{% set _ = smoothed_temp_history.append(soak_temp_smoothed) %}
# round off for display and comparisons later
{% set soak_temp_smoothed = soak_temp_smoothed | round(3) %}
# compute the slope
{% if (smoothed_temp_history | length) > heat_soak.rate_smooth_time %}
{% set smoothed_temp_history = smoothed_temp_history[1:] %}
# Least Squares
# adapted from: https://towardsdatascience.com/linear-regression-using-least-squares-a4c3456e8570
# X values are time in seconds
# Y values are temperatures
{% set count = (smoothed_temp_history | length) %}
{% set times = range(0, count, 1) %}
{% set x_sum = (times | sum) | float %}
{% set y_sum = (smoothed_temp_history | sum) | float %}
# Squares
# Jinja oddity: += doesn't work in loops??, so collect squares and sum after
{% set xx_sum_arr = [] %}
{% set xy_sum_arr = [] %}
{% for i in times %}
{% set x = times[i] %}
{% set y = smoothed_temp_history[i] %}
{% set _ = xx_sum_arr.append(x * x) %}
{% set _ = xy_sum_arr.append(x * y) %}
{% endfor %}
{% set xx_sum = (xx_sum_arr | sum) | float %}
{% set xy_sum = (xy_sum_arr | sum) | float %}
# Slope calculation. Slope is per second so * 60 for rate per minute
{% set slope = (60.0 * ((count | float * xy_sum) - (x_sum * y_sum)) / ((count | float * xx_sum) - (x_sum * x_sum))) | round(3) %}
{% endif %}
{% endif %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=temp_history VALUE="{(temp_history | pprint | replace("\n", ""))}"
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=smoothed_temp_history VALUE="{(smoothed_temp_history | pprint | replace("\n", ""))}"
# while heating
{% if stage == "heating" %}
{% if heater_temp < heat_soak.target_temp %}
{% if total_time_elapsed % heat_soak.heating_report_interval == 0 %}
{% set message = "Heating -- %.1fC / %.1fC -- %.1fm elapsed" % (heater_temp, heat_soak.target_temp, total_time_elapsed / 60.0) %}
M117 {message}
{action_respond_info(message)}
{% endif %}
{% else %}
{action_respond_info("Heating completed after ~%.1fm, starting heat soak phase." % (total_time_elapsed / 60.0))}
# skip the soak phase if resume was pressed
{% set stage = "done" if heat_soak.resume_trigger else "soaking" %}
# reset total time to 0 so soaking time is reported from 0
{% set total_time_elapsed = 0 %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE={ total_time_elapsed }
{% endif %}
{% endif %}
# while soaking
{% if stage == "soaking" %}
## decrement the soak countdown
{% set soak_time_remaining = [heat_soak.soak_time_remaining - heat_soak.check_interval, 0] | max %}
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={ soak_time_remaining }
## abort soaking if timeout is reached
{% if soak_time_remaining <= 0 %}
{action_respond_info("Heat soak timed out after ~%.1fm" % (total_time_elapsed / 60.0))}
{% set stage = "cancel" %}
# if the smoothing algorithm hasn't settled on these values yet, dont evaluate them
{% elif slope is none or soak_temp_smoothed is none %}
{% if total_time_elapsed % heat_soak.soaking_report_interval == 0 %}
## not enough data points yet to determing the soak rate, show dashes
{% set message = "Soaking -- gathering data -- %.1fm remaining" % (soak_time_remaining / 60.0) %}
M117 { message }
{action_respond_info(message)}
{% endif %}
# if the heat soak must continue for some reason
{% elif (slope > heat_soak.target_rate) or (heat_soak.min_soak_temp > 0 and soak_temp_smoothed < heat_soak.min_soak_temp) %}
{% if total_time_elapsed % heat_soak.soaking_report_interval == 0 %}
{% set format_vars = [
soak_temp_smoothed | float,
(heat_soak.min_soak_temp | float | round(0) | string) if (heat_soak.min_soak_temp > 0) else "--",
slope,
heat_soak.target_rate,
soak_time_remaining / 60.0
] %}
{% set message = "Soaking -- temp: {0:.1f}C / {1}C -- rate: {2:.3f}C/m / {3:.3f}C/m -- {4:.1f}m remaining".format(*format_vars) %}
M117 { message }
{action_respond_info(message)}
{% endif %}
## end soaking if the target rate is achieved
{% else %}
{action_respond_info("Heat soak complete after ~%.1fm at %.3fC/m / %.1fC" % (total_time_elapsed / 60.0, slope, soak_temp))}
{% set stage = "done" %}
{% endif %}
{% endif %}
# save stage before calling any macros
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'{stage}'"
{% if stage in ("done", "cancel") %}
STOP_HEAT_SOAK
{% if stage == "cancel" %}
{% if heat_soak.cancel %}
{heat_soak.cancel}
{% elif printer['virtual_sdcard'].is_active %}
CANCEL_PRINT
{% endif %}
{% else %}
{% if heat_soak.complete %}
{heat_soak.complete}
{% endif %}
{% if printer['pause_resume'].is_paused %}
RESUME
{% endif %}
{% endif %}
{% else %}
## trigger ourselves again
UPDATE_DELAYED_GCODE ID=_heat_soaker DURATION={ heat_soak.check_interval }
## dwell for 1ms to prevent from going idle
G4 P1
{% endif %}
# save ending value of stage
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'{stage}'"
+259
View File
@@ -0,0 +1,259 @@
#========================================================
#=====================iHeater cfg========================
#========================================================
#=====================iHeater pins=======================
[board_pins iHeater_Aliases]
mcu: iHeater
aliases:
TH0=PA3, TH1=PA0, TH2=PB1,
HEATER=PA1, FAN=PA2,
MODE=PA4,
LED1=PA7, LED2=PA6, LED3=PA5,
USART1_TX=PA9, USART1_RX=PA10
#=====================iHeater mcu=======================
# [mcu]
# serial: /dev/serial/by-id/usb-Klipper_stm32f042x6_0C0018000D53304347373020-if00
[mcu iHeater] #second MCU
#is_non_critical: true: true # flag this as non critical
serial: /dev/serial/by-id/usb-Klipper_stm32f042x6_0C0020000E43304754393320-if00
restart_method: command
[temperature_sensor iHeater]
sensor_type: temperature_mcu
sensor_mcu: iHeater
#============================================================
#======================= iHeater CONFIG ======================
#============================================================
# Make sure to perform PID calibration for the chamber heater:
#🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
# PID_CALIBRATE HEATER=iHeater_H TARGET=70
#🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
# This is essential for accurate and stable chamber heating.
# Without PID tuning, the temperature may overshoot or take too long to stabilize!
#===========================================================
[heater_generic iHeater_H]
heater_pin: iHeater:HEATER
max_power: 1
sensor_type: NTC 100K MGB18-104F39050L32
sensor_pin: iHeater:TH0
pwm_cycle_time: 0.3
min_temp: 0
max_temp: 120
control: pid
# pid_Kp: 37.948
# pid_Ki: 2.040
# pid_Kd: 176.497
pid_Kp: 26.656
pid_Ki: 0.96
pid_Kd: 184.971
[verify_heater iHeater_H]
max_error: 240
check_gain_time: 1800
heating_gain: 1
[heater_fan chamber_heater_fan]
pin: iHeater:FAN
heater: iHeater_H
heater_temp: 50.0
fan_speed: 1.0
shutdown_speed: 0
kick_start_time: 0.5
[temperature_sensor iHeater_S]
sensor_pin: iHeater:TH1
sensor_type: NTC 100K MGB18-104F39050L32
min_temp: 0
max_temp: 140
# [temperature_sensor iHeater_Chamber_ext]
# sensor_pin: iHeater:TH2
# sensor_type: NTC 100K MGB18-104F39050L32
# min_temp: 0
# max_temp: 140
[gcode_button mode_button]
pin: iHeater:MODE
# debounce_delay: 50
press_gcode: iHEATER_OFF
# [output_pin led_1]
# pin: iHeater:LED1
# [output_pin led_2]
# pin: iHeater:LED2
# [output_pin led_3]
# pin: iHeater:LED3
#######################################################
##########################SETUP########################
#######################################################
[gcode_macro CHAMBER_VARS]
variable_chamber_target: 0 # target chamber temperature, °C
variable_start_offset: 10 # printing can start when chamber reaches (target - offset)
variable_delta_temp: 10 # delta between chamber and heater, °C
variable_min_heater_temp: 50 # minimum heater temperature (for cooling), °C
variable_max_heater_temp: 100 # maximum heater temperature, °C
variable_control_interval: 1.0 # interval for control function call, seconds
variable_air_min_delta: 0.5 # minimum difference between desired and current chamber temperature (heater temp = desired + delta_temp), °C
variable_air_max_delta: 5.0 # maximum difference between desired and current chamber temperature (heater temp = max_heater_temp), °C
gcode:
##########################MACRO########################
[gcode_macro M141]
#rename_existing: M141.1
gcode:
# Get desired chamber temperature:
# - if parameter S is passed, use it
# - otherwise use chamber_target from CHAMBER_VARS
{% set chamber_target = params.S|default(printer["gcode_macro CHAMBER_VARS"].chamber_target)|int %}
# Get delta temperature:
# - if parameter D is passed, use it
# - otherwise use delta_temp from CHAMBER_VARS
{% set delta_temp = params.D|default(printer["gcode_macro CHAMBER_VARS"].delta_temp)|int %}
# Get maximum heater temperature:
# - if parameter H is passed, use it
# - otherwise use max_heater_temp from CHAMBER_VARS
{% set max_heater_temp = params.H|default(printer["gcode_macro CHAMBER_VARS"].max_heater_temp)|int %}
# Get interval for control function call
{% set control_interval = printer["gcode_macro CHAMBER_VARS"].control_interval|float %}
RESPOND prefix="M141" msg="S{chamber_target} D{delta_temp} H{max_heater_temp}"
# Save parameters to CHAMBER_VARS for delayed_gcode
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=chamber_target VALUE={chamber_target}
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=delta_temp VALUE={delta_temp}
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=max_heater_temp VALUE={max_heater_temp}
# Initial calculation
iHEATER_CONTROL
# Start temperature control macro
UPDATE_DELAYED_GCODE ID=_iHEATER_CONTROL DURATION={control_interval}
[gcode_macro M191]
#rename_existing: M191.1
gcode:
# Get desired chamber temperature:
# - if parameter S is passed, use it
# - otherwise use chamber_target from CHAMBER_VARS
{% set chamber_target = params.S|default(printer["gcode_macro CHAMBER_VARS"].chamber_target)|int %}
# Get delta temperature:
# - if parameter D is passed, use it
# - otherwise use delta_temp from CHAMBER_VARS
{% set delta_temp = params.D|default(printer["gcode_macro CHAMBER_VARS"].delta_temp)|int %}
# Get maximum heater temperature:
# - if parameter H is passed, use it
# - otherwise use max_heater_temp from CHAMBER_VARS
{% set max_heater_temp = params.H|default(printer["gcode_macro CHAMBER_VARS"].max_heater_temp)|int %}
# Get minimum chamber temperature for print start:
# - if parameter W is passed, use it
# - otherwise use start_offset from CHAMBER_VARS
{% set start_offset = params.W|default(printer["gcode_macro CHAMBER_VARS"].start_offset)|int %}
RESPOND prefix="M191" msg="S{chamber_target} D{delta_temp} H{max_heater_temp} W{start_offset}"
M141 S{chamber_target} D{delta_temp} H{max_heater_temp}
# Save parameter to CHAMBER_VARS
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=start_offset VALUE={start_offset}
# Wait until chamber reaches the target temperature
TEMPERATURE_WAIT SENSOR="temperature_sensor iHeater_S" MINIMUM={chamber_target - start_offset}
RESPOND prefix="M191" msg="Chamber reached {chamber_target - start_offset}°C"
[gcode_macro iHEATER_ON]
description: Alternative to M141 & M191 with alternative parameter names
gcode:
{% set s = params.CHAMBER_TEMP|default(printer["gcode_macro CHAMBER_VARS"].chamber_target)|int %}
{% set d = params.DELTA|default(printer["gcode_macro CHAMBER_VARS"].delta_temp)|int %}
{% set h = params.HEATER_MAX|default(printer["gcode_macro CHAMBER_VARS"].max_heater_temp)|int %}
{% set w = params.CHAMBER_AWAIT_TEMP|default(printer["gcode_macro CHAMBER_VARS"].start_offset)|int %}
{% if w > 0 %}
# If w is greater than 0, use M191 with wait for minimum temperature
M191 S{s} D{d} H{h} W{s - w}
{% else %}
# Otherwise use M141 without waiting
M141 S{s} D{d} H{h}
{% endif %}
[gcode_macro iHEATER_OFF]
gcode:
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=chamber_target VALUE=0
# SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=start_offset VALUE=0
UPDATE_DELAYED_GCODE ID=_iHEATER_CONTROL DURATION=1
RESPOND prefix="iHeater_off" msg="Heating disabled."
[gcode_macro iHEATER_CONTROL]
gcode:
# Target chamber temperature
{% set target_chamber_temp = printer["gcode_macro CHAMBER_VARS"].chamber_target|float %}
# Delta - how much the heater temp exceeds chamber temp
{% set delta_temp = printer["gcode_macro CHAMBER_VARS"].delta_temp|float %}
# Minimum heater temperature
{% set min_heater_temp = printer["gcode_macro CHAMBER_VARS"].min_heater_temp|float %}
# Maximum heater temperature
{% set max_heater_temp = printer["gcode_macro CHAMBER_VARS"].max_heater_temp|float %}
# Current heater temperature
{% set heater_temp = printer["heater_generic iHeater_H"].temperature|float %}
# Current chamber air temperature (from sensor)
{% set chamber_temp = printer["temperature_sensor iHeater_S"].temperature|float %}
# Control function call interval
{% set control_interval = printer["gcode_macro CHAMBER_VARS"].control_interval|float %}
# Minimum difference between desired and current chamber temperature
{% set air_min_delta = printer["gcode_macro CHAMBER_VARS"].air_min_delta|float %}
# Maximum difference between desired and current chamber temperature
{% set air_max_delta = printer["gcode_macro CHAMBER_VARS"].air_max_delta|float %}
# Variable to set target heater temperature
{% set target_heater_temp = 0.0 %}
# RESPOND prefix="iHeater_control" msg="Heater control..."
{% if target_chamber_temp > 0 %}
# Heating is active
{% set temp_diff = target_chamber_temp - chamber_temp %}
# MapToRange
{% set value = temp_diff %}
{% set inputLowerBound = air_min_delta %}
{% set inputUpperBound = air_max_delta %}
{% set outputLowerBound = delta_temp %}
{% set outputUpperBound = [delta_temp, max_heater_temp - target_chamber_temp]|max %}
{% set slope = (outputUpperBound - outputLowerBound) / (inputUpperBound - inputLowerBound) %}
{% set clamped = ([inputLowerBound, value, inputUpperBound]|sort)[1] %}
{% set adjustment = outputLowerBound + slope * (clamped - inputLowerBound) %}
{% set target_heater_temp = (target_chamber_temp + adjustment)|round(2) %}
{% elif heater_temp > min_heater_temp %}
# Heating is off, but heater hasn't cooled down yet
RESPOND prefix="iHeater_control" msg="Cooling down the heater..."
{% else %}
# Heater cooled down - end macro
RESPOND prefix="iHeater_control" msg="Heater cooldown complete."
{% set control_interval = 0.0 %}
{% endif %}
SET_HEATER_TEMPERATURE HEATER=iHeater_H TARGET={target_heater_temp}
UPDATE_DELAYED_GCODE ID=_iHEATER_CONTROL DURATION={control_interval}
[delayed_gcode _iHEATER_CONTROL]
gcode:
iHEATER_CONTROL
+26
View File
@@ -0,0 +1,26 @@
#####################################################################
# klipper-macros: include this one file from printer.cfg
#
# [include klipper-macros/klipper-macros.cfg]
#
# The order below matters: several files rename PAUSE/RESUME/CANCEL_PRINT
# (rename_existing), and the last definition wins. It is the order these
# files had on the original printer. Comment out what you do not use.
#
# Not part of this repo, include them yourself if you use them:
# timelapse.cfg (moonraker-timelapse), TEST_SPEED.cfg (Ellis),
# mainsail.cfg (before this file), blobifier, AFC, KAMP
#####################################################################
[include bedfans.cfg]
[include stealthburner_led_effects_barf.cfg]
[include macros_klipperscreen.cfg]
[include iHeater.cfg]
[include drying.cfg]
[include afc_macros.cfg]
[include motion.cfg]
[include heatsoak.cfg]
[include purge_until_empty.cfg]
[include purge_check.cfg]
[include macros.cfg]
[include bd_pa_afc_spoolman.cfg]
+278
View File
@@ -0,0 +1,278 @@
# Print start/end macros. Included by klipper-macros.cfg.
# [dynamicmacros]
# configs: Macros/dynamicmacros.cfg
# [gcode_button kill_switch]
# pin: gpio7
# press_gcode: M112
[idle_timeout]
gcode:
TURN_OFF_HEATERS
iHEATER_OFF ; Turns off the chamber heater
M84
# reset paused flag
{% if printer.pause_resume.is_paused|lower == 'false' and park|lower == 'true'%}
_TOOLHEAD_PARK_PAUSE_CANCEL
{% endif %}
_status_ready
# A list of G-Code commands to execute on an idle timeout. See
# docs/Command_Templates.md for G-Code format. The default is to run
# "TURN_OFF_HEATERS" and "M84".
timeout: 85400 # 24 hours
# Idle time (in seconds) to wait before running the above G-Code
# commands. The default is 600 seconds.
#####################################################################
# Macros
#####################################################################
# [gcode_macro PRINT_START]
# gcode:
# PRINT_WARMUP BED={params.BED} EXTRUDER={params.EXTRUDER} CHAMBER={params.CHAMBER|default("0")} TOOL={params.TOOL|default("0")}
# PRINT_START_2 EXTRUDER={params.EXTRUDER}
[gcode_macro PRINT_WARMUP]
description: Perform initial homing and heating tasks
gcode:
{% set target_bed = params.BED|int %}
{% set target_extruder = params.EXTRUDER|int %}
{% set target_chamber = params.CHAMBER|default("0")|int %}
# {% set x_wait = printer.toolhead.axis_maximum.x|float / 2 %}
# {% set y_wait = printer.toolhead.axis_maximum.y|float / 2 %}
{% set initial_tool = params.TOOL|default("0")|int %}
{% set chamber_temp = printer["temperature_sensor chamber"].temperature|float %}
RESET_FAILURE
CLEAR_PAUSE
_motion_detected
#SET_PIN PIN=caselight VALUE=1.00
UPDATE_DELAYED_GCODE ID=_delayed_motors_off DURATION=0
BED_MESH_CLEAR
SET_GCODE_OFFSET Z=0
CLEAR_PAUSE
SKEW_PROFILE LOAD=Califlower
{% set current_temp = printer.extruder.temperature %}
{% if current_temp > 150 %}
RESPOND MSG="nozzle to hot, lowering to 150"
M104 S150 ; set nozzle temp to 150
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
{% endif %}
_status_homing
RESPOND MSG="homing"
HOME
RESPOND MSG="homing finished"
# G0 Z2 ; position beacon at 2mm for heat soak
{% if target_bed < 60 %}
RESPOND MSG="Using PID for <60°C bed"
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={target_bed} PID_KP=33.606 PID_KI=0.694 PID_KD=407.058
{% else %}
RESPOND MSG="Using PID for ≥60°C bed"
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={target_bed} pid_Kp=46.843 pid_Ki=2.002 pid_Kd=274.034
{% endif %}
G1 X175 Y175 Z2 F1200
_status_heating
RESPOND MSG="heating"
{% if target_bed > 0 %}
M140 S{target_bed} ; start bed heater
M104 S150 ; set nozzle temp to 150
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
M190 S{target_bed} ; wait on bed temperature
{% endif %}
HOME
QUAD_GANTRY_LEVEL
BLOBIFIER_CLEAN ; call another macro to wipe nozzle if available
G1 X175 Y175 Z5 F1200
T{initial_tool} #PURGE_LENGTH=100 #Load Initial Tool
M104 S{target_extruder} ; set extruder to print temp
M109 S{target_extruder} ; wait for extruder temp
G10 ; retract
M104 S150 ; set nozzle temp to 150
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
BLOBIFIER_CLEAN ; call another macro to wipe nozzle if available
G1 X175 Y175 Z5 F1200
{% if target_chamber > 30 %}
RESPOND MSG="waiting for chamber temperature"
M141 S{target_chamber}
# M191 S{target_chamber} ; Waits until the chamber temperature reaches 60°C
# TEMPERATURE_WAIT SENSOR="temperature_sensor chamber" MINIMUM={target_chamber}
M106 S255
# HEAT_SOAK HEATER='heater_bed' TARGET={BED_TEMP} SOAKER='temperature_sensor iHeater_S' SOAK_TEMP={target_chamber} TIMEOUT=60 CANCEL='_NOTHING'
HEAT_SOAK HEATER='heater_bed' TARGET={target_bed} SOAKER='temperature_sensor chamber' SOAK_TEMP={target_chamber} TIMEOUT=60 CANCEL='_NOTHING'
# (CANCEL='_NOTHING') and having unreachable SOAK_TEMP makes it run till the TIMEOUT and continue without canceling the print.
{% else %}
RESPOND MSG="waiting for bed core temperature"
HEAT_SOAK HEATER='heater_bed' TARGET={target_bed} SOAKER='temperature_sensor bed_core' SOAK_TEMP={target_bed - 5} TIMEOUT=30 CANCEL='_NOTHING'
# (CANCEL='_NOTHING') and having unreachable SOAK_TEMP makes it run till the TIMEOUT and continue without canceling the print.
{% endif %}
[gcode_macro _NOTHING]
gcode:
_HEAT_SOAK__BASE_RESUME
[gcode_macro PRINT_START]
gcode:
RESPOND MSG="print start macro continued"
_motion_detected
{% set target_extruder = params.EXTRUDER|int %}
M106 S0
M104 S150 ; set nozzle temp to 150
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
HOME
BLOBIFIER_CLEAN ; call another macro to wipe nozzle if available
_status_calibrating_z
G28 Z METHOD=CONTACT CALIBRATE=1 ; calibrate z offset and beacon model hot
_status_leveling
RESPOND MSG="quad ganrty level"
QUAD_GANTRY_LEVEL
_status_meshing
RESPOND MSG="meshing"
BED_MESH_CALIBRATE RUNS=2 ; bed mesh in scan mode
BLOBIFIER_CLEAN ; call another macro to wipe nozzle if available
M104 S150 ; set nozzle temp to 150
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
_status_calibrating_z
G28 Z METHOD=CONTACT CALIBRATE=1 ; calibrate z offset and beacon model hot
_status_heating
M104 S{target_extruder} ; set extruder to print temp
M109 S{target_extruder} ; wait for extruder temp
# ; needs fine tuning, long meltzones require more
# SET_GCODE_OFFSET Z_ADJUST={OFFSET} ; apply optional material squish via slicer
#SET_GCODE_OFFSET Z=-0.06 ; squish
# SET_GCODE_OFFSET Z=0.02 ; squish # worked before but a bit to high now
# SET_GCODE_OFFSET Z=-0.05 ; squish
SET_GCODE_OFFSET Z=-0.00 ; squish
RESPOND MSG="starting"
;PRIME_NOZZLE ; call another macro to purge or prime nozzle
# WIPE_NOZZLE ; call another macro to wipe nozzle if available
# Line_Purge
# AFC_POOP PURGE_LENGTH=100
# AFC_KICK
BLOBIFIER_CLEAN
PA_CALIBRATE_ALL_LANES LANES="lane1,lane2,lane3,lane4,lane5"
M104 S{target_extruder} ; set extruder to print temp
M109 S{target_extruder} ; wait for extruder temp
BLOBIFIER_CLEAN
; start printing
G90 ; absolute positioning
G1 Z20 F30000 ; move nozzle away from bed
G1 X175 Y175 F30000 ; move nozzle away from bed
G28 XYZ
#G11 ; unretract
_AFC_STORE_HOME_REFERENCE
SET_BDWIDTH NAME=fila_width_0 COMMAND=ENABLE_ALL
# SET_BDWIDTH NAME=fila_width_0 COMMAND=ENABLE_MOTION
_status_printing
[gcode_macro PRINT_END]
# Use PRINT_END for the slicer ending script - please customise for your slicer of choice
gcode:
# safe anti-stringing move coords
{% set th = printer.toolhead %}
{% set x_safe = th.position.x + 20 * (1 if th.axis_maximum.x - th.position.x > 20 else -1) %}
{% set y_safe = th.position.y + 20 * (1 if th.axis_maximum.y - th.position.y > 20 else -1) %}
{% set z_safe = [th.position.z + 2, th.axis_maximum.z]|min %}
SAVE_GCODE_STATE NAME=STATE_PRINT_END
_motion_detected
M221 S100
M400 ; wait for buffer to clear
G92 E0 ; zero the extruder
# G1 E-0.5 F1800 ; retract filament
G10 ; retract
TURN_OFF_HEATERS
iHEATER_OFF ; Turns off the chamber heater
G90 ; absolute positioning
G0 X{x_safe} Y{y_safe} Z{z_safe} F20000 ; move nozzle to remove stringing
G0 X{th.axis_maximum.x//2} Y{th.axis_maximum.y - 5} F3600 ; park nozzle at rear
M107 ; turn off fan
{% if printer.pause_resume.is_paused|lower == 'false' and park|lower == 'true'%}
_TOOLHEAD_PARK_PAUSE_CANCEL
{% endif %}
SET_BDWIDTH NAME=fila_width_0 COMMAND=DISABLE ;disable the bdwidth
#SET_PIN PIN=caselight VALUE=0.03
_status_part_ready
UPDATE_DELAYED_GCODE ID=_delayed_motors_off DURATION=3600
BED_MESH_CLEAR
SET_SKEW CLEAR=1
RESTORE_GCODE_STATE NAME=STATE_PRINT_END
[delayed_gcode _delayed_motors_off]
gcode:
M84
# [gcode_macro QUAD_GANTRY_LEVEL]
# rename_existing: _QUAD_GANTRY_LEVEL
# gcode:
# _QUAD_GANTRY_LEVEL
# _QUAD_GANTRY_LEVEL PROBE_METHOD=contact
### bed warp visualizer
[gcode_macro SOAK_MY_BED]
description: Heat soak bed and live meassure
rename_existing: SOAK_MY_BED_BASE
gcode:
{% set temp = params.TEMPERATURE|default(60) %}
{% set duration = params.DURATION|default(10) %}
{% set heater = params.HEATER|default("heater_bed") %}
{% set mesh = params.MESH_COMMAND|default("BED_MESH_CALIBRATE METHOD=beacon") %}
G28
QUAD_GANTRY_LEVEL
SOAK_MY_BED_BASE TEMPERATURE={temp} DURATION={duration} HEATER={heater} MESH_COMMAND="{mesh}"
+174
View File
@@ -0,0 +1,174 @@
# https://klipperscreen.readthedocs.io/en/latest/macros/#recommended
[gcode_macro PAUSE]
description: Pause the actual running print
rename_existing: PAUSE_BASE
variable_saved_fan_speed: 0.0
gcode:
# SAVE_GCODE_STATE NAME=PAUSE_MACRO_state
# Save current fan speed
{% set current_fan = printer.fan.speed %}
SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=saved_fan_speed VALUE={current_fan}
# Current position
{% set x = printer.toolhead.position.x|float %}
{% set y = printer.toolhead.position.y|float %}
# Determine target positions
{% set target_x = 340 if (x < 340 or x > 350) else x %}
{% set target_y = 340 if (y < 340 or y > 350) else y %}
# Only move if needed
{% if target_x != x or target_y != y %}
G90
G1 X{target_x} Y{target_y} F12000
{% endif %}
# Pause print
PAUSE_BASE
_TOOLHEAD_PARK_PAUSE_CANCEL
# Set fan to S28 (28/255 ≈ 0.1098)
M106 S28
[gcode_macro RESUME]
description: Resume the actual running print
rename_existing: RESUME_BASE
variable_skip_restore: 0
gcode:
{% set extrude = printer['gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL'].extrude %}
{% set skip = printer['gcode_macro RESUME'].skip_restore|int %}
#### get VELOCITY parameter if specified ####
{% if 'VELOCITY' in params|upper %}
{% set get_params = ('VELOCITY=' + params.VELOCITY) %}
{%else %}
{% set get_params = "" %}
{% endif %}
##### end of definitions #####
{% if printer.extruder.can_extrude|lower == 'true' %}
M83
G1 E{extrude} F2100
{% if printer.gcode_move.absolute_extrude |lower == 'true' %} M82 {% endif %}
{% else %}
{action_respond_info("Extruder not hot enough")}
{% endif %}
#BLOBIFIER_CLEAN
#G1 Z10
# Restore saved fan speed
{% set old_fan = printer["gcode_macro PAUSE"].saved_fan_speed %}
{% if old_fan > 0 %}
{% set fan_pwm = (old_fan * 255) | int %}
{% if fan_pwm > 255 %}
{% set fan_pwm = 255 %}
{% endif %}
M106 S{fan_pwm}
{% else %}
M107
{% endif %}
{% if skip == 0 %}
# RESTORE_GCODE_STATE name=PAUSE_MACRO_state
RESUME_BASE {get_params}
{% else %}
RESPOND PREFIX="AFC" MSG="Skipping RESTORE_GCODE_STATE (drift resume)"
{% endif %}
_status_printing
CLEAR_PAUSE
RESET_FAILURE
SET_GCODE_VARIABLE MACRO=RESUME VARIABLE=skip_restore VALUE=0
[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
description: Helper: park toolhead used in PAUSE and CANCEL_PRINT
variable_extrude: 2.0
gcode:
##### set park positon for x and y #####
# default is your max posion from your printer.cfg
# {% set x_park = printer.toolhead.axis_maximum.x|float - 5.0 %}
{% set x_park = 20.0 %}
{% set y_park = printer.toolhead.axis_maximum.y|float - 5.0 %}
{% set z_park_delta = 2.0 %}
##### calculate save lift position #####
{% set max_z = printer.toolhead.axis_maximum.z|float %}
{% set act_z = printer.toolhead.position.z|float %}
{% if act_z < (max_z - z_park_delta) %}
{% set z_safe = z_park_delta %}
{% else %}
{% set z_safe = max_z - act_z %}
{% endif %}
##### end of definitions #####
{% if printer.extruder.can_extrude|lower == 'true' %}
M83
G1 E-{extrude} F2100
{% if printer.gcode_move.absolute_extrude |lower == 'true' %} M82 {% endif %}
{% else %}
{action_respond_info("Extruder not hot enough")}
{% endif %}
{% if "xyz" in printer.toolhead.homed_axes %}
G91
G1 Z{z_safe} F900
G90
G1 X{x_park} Y{y_park} F6000
{% if printer.gcode_move.absolute_coordinates|lower == 'false' %} G91 {% endif %}
{% else %}
{action_respond_info("Printer not homed")}
{% endif %}
[gcode_macro CANCEL_PRINT]
description: Cancel the actual running print
rename_existing: CANCEL_PRINT_BASE_KLIPPERSCREEN
variable_park: True
gcode:
## Move head and retract only if not already in the pause state and park set to true
{% if printer.pause_resume.is_paused|lower == 'false' and park|lower == 'true'%}
_TOOLHEAD_PARK_PAUSE_CANCEL
{% endif %}
STOP_HEAT_SOAK
TURN_OFF_HEATERS
CANCEL_PRINT_BASE_KLIPPERSCREEN
[gcode_macro LOAD_FILAMENT]
variable_load_distance: 60
variable_purge_distance: 20
gcode:
{% set speed = params.SPEED|default(15) %}
{% set max_velocity = printer.configfile.settings['extruder'].max_extrude_only_velocity * 15 %}
SAVE_GCODE_STATE NAME=load_state
G91
G92 E0
G1 E{load_distance} F{max_velocity} # fast-load
G1 E{purge_distance} F{speed} # purge
RESTORE_GCODE_STATE NAME=load_state
[gcode_macro UNLOAD_FILAMENT]
variable_unload_distance: 100
variable_purge_distance: 0
gcode:
{% set speed = params.SPEED|default(15) %}
{% set max_velocity = printer.configfile.settings['extruder'].max_extrude_only_velocity * 30 %}
SAVE_GCODE_STATE NAME=unload_state
G91
G92 E0
G1 E{purge_distance} F{speed} # purge
G1 E-20 F100# unload
G1 E-{unload_distance} F{max_velocity} # fast-unload
RESTORE_GCODE_STATE NAME=unload_state
[gcode_macro M600]
#
# M600
# https://github.com/simplisticton/common-macros/tree/main
#
# Mimic Marlin's Pause for filament change command
description: Pause for filament change
gcode:
PAUSE
M117 Paused
M118 Print paused for filament change... type "Resume" to continue
+40
View File
@@ -0,0 +1,40 @@
[gcode_button am312]
pin: !PC0
press_gcode:
_motion_detected
[gcode_macro _motion_detected]
description: Motion sensor triggered
gcode:
{% set printing = printer.print_stats.state == "printing" %}
# RESPOND MSG="AM312: Motion detected"
{% if printing %}
# RESPOND MSG="AM312: Printer is printing -> light ON (100%)"
SET_PIN PIN=caselight VALUE=1.0
_set_nozzle_leds_on
{% else %}
# RESPOND MSG="AM312: Printer idle -> light ON (10%)"
SET_PIN PIN=caselight VALUE=0.1
_set_nozzle_leds_off
{% endif %}
# RESPOND MSG="AM312: Resetting timeout to 30 seconds"
UPDATE_DELAYED_GCODE ID=_motion_timeout DURATION=30
[delayed_gcode _motion_timeout]
gcode:
{% set printing = printer.print_stats.state == "printing" %}
# RESPOND MSG="AM312: Timeout reached, no motion"
{% if printing %}
# RESPOND MSG="AM312: Printing -> dim light to 10%"
SET_PIN PIN=caselight VALUE=0.1
{% else %}
# RESPOND MSG="AM312: Idle -> light OFF"
SET_PIN PIN=caselight VALUE=0.0
{% endif %}
_set_nozzle_leds_off
+155
View File
@@ -0,0 +1,155 @@
#####################################################################
# Purge check - did the filament really reach the nozzle during the
# Blobifier purge after a tool change?
#
# Two independent checks around the purge AFC runs after every load:
# 1. bdwidth motion: extruded mm vs mm the sensor saw moving past it.
# Catches a grinding extruder, a tangle or a snapped filament.
# 2. bd_pressure (needs the load-check firmware extension): how long
# the nozzle pressure stayed above the threshold. Catches filament
# that never reached the melt zone. Skipped automatically on stock
# firmware or when bdpressure_check.py is not installed.
#
# Needs: bdwidth_status.py in ~/klipper/klippy/extras/
# bdpressure_check.py in ~/klipper/klippy/extras/ (optional)
# (klipper/install.sh links both; guide: klipper/load_check/afc_blobifier/README.md)
# Hook: in [AFC] (AFC/AFC.cfg) change poop_cmd: BLOBIFIER
# to poop_cmd: BLOBIFIER_CHECKED
#
# Rollout:
# Phase 1 mode "report": only prints the results after every purge.
# Collect a few days of values, then set min_ratio and
# pressure_min_ms well below the lowest good ones.
# Phase 2 mode "enforce": pauses when a check fails.
#
# Costs about 2 x (sample_time + 0.5 s) per swap, 5 s at sample_time 2,
# so the sensors are quiet and up to date before and after the purge.
# When bdwidth is not reading, add bdwidth_warmup_ms: it is switched on for
# the check (motion only, SET_BDWIDTH ENABLE_MOTION) with its runout pause
# off meanwhile (SET_FILAMENT_SENSOR), and outside a print off again after.
#####################################################################
[bdwidth_status]
sensor: fila_width_0
# Remove this section if bdpressure_check.py is not installed
[bdpressure_check]
sensor: bd_pa
[gcode_macro _PURGE_CHECK_CFG]
variable_mode: "enforce" # report | enforce | off
variable_motion_check: False # False: skip the bdwidth motion check, pressure only
variable_min_ratio: 0.4 # placeholder: sensed mm / extruded mm below this = not feeding
variable_min_extrude: 20 # skip the checks for purges shorter than this (mm)
variable_pressure_min_ms: 5000 # placeholder: longest continuous pressure needed (ms)
# bdwidth is not reading after SET_BDWIDTH ... COMMAND=DISABLE and after
# every Klipper restart until an ENABLE (outside a print, often early in a
# print start). The check then switches it on and waits this long for the
# first readings (its first read often fails and it retries after 10 s).
variable_bdwidth_warmup_ms: 15000
variable_start_counts: 0
variable_start_e: 0.0
gcode:
[gcode_macro BLOBIFIER_CHECKED]
description: Blobifier purge with bdwidth motion and bd_pressure checks (use as poop_cmd in [AFC])
gcode:
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
{% set wait = (printer.bdwidth_status.sample_time * 1000 + 500)|int %}
{% set pressure = 'bdpressure_check' in printer %}
{% set bdw = printer.bdwidth_status.name %}
# If bdwidth is not reading (outside a print, early in a print start,
# after a Klipper restart) it is switched on for the check, with its
# runout response off meanwhile, so an empty lane does not make bdwidth
# pause the printer (the check reports it instead). Afterwards the
# runout response is restored; bdwidth itself is only switched off
# again outside a print, so a print never loses it.
{% set bdw_on = cfg.motion_check and not printer.bdwidth_status.reading|default(false) %}
{% set bdw_off = bdw_on and printer.print_stats.state not in ["printing", "paused"] %}
{% set runout_was = 1 if printer.bdwidth_status.enabled else 0 %}
# Without the motion check, bdwidth's own runout response is off during
# the purge too, so a bdwidth that does not see the movement cannot
# pause the printer here; the pressure check judges the purge.
{% set runout_off = bdw_on or not cfg.motion_check %}
{% if cfg.mode == "off" %}
BLOBIFIER {rawparams}
{% else %}
M400
{% if runout_off %}
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE=0
{% endif %}
{% if bdw_on %}
SET_BDWIDTH NAME={bdw} COMMAND=ENABLE_MOTION
G4 P{cfg.bdwidth_warmup_ms}
{% endif %}
G4 P{wait}
_PURGE_CHECK_START
{% if pressure %}
BDP_CHECK_START
{% endif %}
BLOBIFIER {rawparams}
M400
{% if pressure %}
BDP_CHECK_STOP
{% endif %}
G4 P{wait}
_PURGE_CHECK_END
{% if bdw_off %}
SET_BDWIDTH NAME={bdw} COMMAND=DISABLE
{% endif %}
{% if runout_off %}
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE={runout_was}
{% endif %}
{% endif %}
# Separate macros so the status is read when they run, not when
# BLOBIFIER_CHECKED is rendered
[gcode_macro _PURGE_CHECK_START]
gcode:
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=start_counts VALUE={printer.bdwidth_status.total_move}
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=start_e VALUE={printer.toolhead.position.e}
[gcode_macro _PURGE_CHECK_END]
gcode:
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
{% set s = printer.bdwidth_status %}
{% set lane = printer.AFC.current_load|default("?", true) %}
{% set extruded = printer.toolhead.position.e - cfg.start_e %}
{% set failed = [] %}
{% if extruded < cfg.min_extrude %}
RESPOND MSG="PURGE_CHECK {lane}: only {'%.1f' % extruded} mm extruded, skipped"
{% else %}
{% set motion_on = cfg.motion_check and s.active != "disable" %}
{% if not cfg.motion_check %}
{% set motion = "motion check off" %}
{% elif not motion_on %}
{% set motion = "motion skipped (bdwidth disabled)" %}
{% else %}
{% set sensed = (s.total_move - cfg.start_counts)|abs / s.linear_motion %}
{% set ratio = sensed / extruded %}
{% set motion = "motion %.1f/%.1f mm ratio %.2f, width %.2f" % (sensed, extruded, ratio, s.diameter) %}
{% if ratio < cfg.min_ratio %}
{% if s.total_move == 0 and s.diameter == 0 %}
{% set _ = failed.append("no data from bdwidth since Klipper start (sensor connected?)") %}
{% else %}
{% set _ = failed.append("filament not moving past bdwidth") %}
{% endif %}
{% endif %}
{% endif %}
{% if 'bdpressure_check' in printer and printer.bdpressure_check.available %}
{% set p = printer.bdpressure_check %}
{% set press = "pressure longest %d ms, total %d ms, peak %d" % (p.longest_ms, p.above_ms, p.peak) %}
{% if p.longest_ms < cfg.pressure_min_ms %}
{% set _ = failed.append("no sustained nozzle pressure") %}
{% endif %}
{% else %}
{% set press = "pressure skipped (no check firmware)" %}
{% endif %}
{% set checked = motion_on or 'bdpressure_check' in printer and printer.bdpressure_check.available %}
{% set verdict = 'FAIL: ' ~ failed|join(', ') if failed else ('OK' if checked else 'NOTHING CHECKED') %}
RESPOND MSG="PURGE_CHECK {lane}: {motion}; {press} -> {verdict}"
{% if failed and cfg.mode == "enforce" %}
RESPOND TYPE=error MSG="PURGE_CHECK: {lane} {failed|join(', ')} - check extruder and lane"
PAUSE
{% endif %}
{% endif %}
+127
View File
@@ -0,0 +1,127 @@
#####################################################################
# PURGE_UNTIL_EMPTY - purge a broken filament out through the nozzle
#
# When the filament snaps inside the PTFE tube, the piece below the break
# is still in the extruder. This macro repeats the Blobifier purge and
# measures each one with bd_pressure (load-check firmware). It stops after
# the first purge without sustained nozzle pressure: the extruder no longer
# has filament to push.
#
# What stays behind: the extruder can only push filament its gears still
# grip. Once the tail has passed the gears, the last bit between the gears
# and the nozzle stays in the hotend. The next normal load pushes it out.
#
# The AFC lane is unsynced first (UNSET_LANE_LOADED). Otherwise AFC keeps
# feeding the lane together with the extruder, the spool side of the break
# can catch up and the purge never ends. Afterwards AFC sees no lane loaded:
# eject or reload the lane as usual.
#
# bdwidth sits at the start of the tube, so after the break it sees no
# filament while the extruder keeps pushing. Its runout response (motion or
# width) would pause the printer mid-purge, so it is switched off during the
# purges (SET_FILAMENT_SENSOR) and restored afterwards. bdwidth is not used
# to decide when to stop: only the nozzle pressure can see the last piece.
#
# Needs: bdpressure_check.py + [bdpressure_check] (see purge_check.cfg)
# Blobifier, homed printer, hot nozzle (the macro heats if needed)
#
# Usage (printer idle or paused, not while printing):
# PURGE_UNTIL_EMPTY defaults below
# PURGE_UNTIL_EMPTY TEMP=240 MAX=10 filament temperature, purge limit
# PURGE_UNTIL_EMPTY LENGTH=100 mm per purge (default: Blobifier's)
# PURGE_UNTIL_EMPTY UNSYNC=0 keep the AFC lane synced
#
# Each purge prints one line; the end result says whether the nozzle ran
# empty or MAX purges were reached (lane still feeding, or nozzle blocked).
#####################################################################
[gcode_macro PURGE_UNTIL_EMPTY]
description: Repeat the Blobifier purge until bd_pressure measures no more filament
variable_max_purges: 15 # safety limit: ~1.2 m of bowden is ~8 purges of 150 mm
variable_empty_ms: 5000 # longest pressure below this = no filament (good purge ~17 s, empty ~30 ms)
variable_done: 0
variable_count: 0
gcode:
{% set cfg = printer["gcode_macro PURGE_UNTIL_EMPTY"] %}
{% set max_purges = params.MAX|default(cfg.max_purges)|int %}
{% set length = params.LENGTH|default(0)|float %}
{% set unsync = params.UNSYNC|default(1)|int %}
{% set min_temp = printer.configfile.settings.extruder.min_extrude_temp|float %}
{% set temp = params.TEMP|default([printer.extruder.target, 0]|max)|float %}
{% if 'bdpressure_check' not in printer %}
{action_raise_error("PURGE_UNTIL_EMPTY: needs bdpressure_check.py and [bdpressure_check]")}
{% elif printer.print_stats.state == "printing" %}
{action_raise_error("PURGE_UNTIL_EMPTY: printer is printing, pause the print first")}
{% elif "xyz" not in printer.toolhead.homed_axes %}
{action_raise_error("PURGE_UNTIL_EMPTY: home the printer first")}
{% elif temp < min_temp %}
{action_raise_error("PURGE_UNTIL_EMPTY: give the filament temperature, e.g. PURGE_UNTIL_EMPTY TEMP=240")}
{% else %}
BDP_CHECK_QUERY
_PURGE_UNTIL_EMPTY_RUN MAX={max_purges} LENGTH={length} UNSYNC={unsync} TEMP={temp}
{% endif %}
# Rendered after BDP_CHECK_QUERY, so 'available' is current.
[gcode_macro _PURGE_UNTIL_EMPTY_RUN]
gcode:
{% if not printer.bdpressure_check.available %}
{action_raise_error("PURGE_UNTIL_EMPTY: bd_pressure load-check firmware not found")}
{% endif %}
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=done VALUE=0
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE=0
{% if params.UNSYNC|int and printer.AFC.current_load %}
RESPOND MSG="PURGE_UNTIL_EMPTY: unsyncing {printer.AFC.current_load} from the extruder (UNSET_LANE_LOADED)"
UNSET_LANE_LOADED
{% endif %}
{% set bdw = printer.bdwidth_status.name if 'bdwidth_status' in printer else '' %}
{% set runout_was = 1 if bdw and printer.bdwidth_status.enabled else 0 %}
{% if runout_was %}
RESPOND MSG="PURGE_UNTIL_EMPTY: bdwidth runout response off during the purges"
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE=0
{% endif %}
M109 S{params.TEMP}
{% for i in range(params.MAX|int) %}
_PURGE_UNTIL_EMPTY_STEP LENGTH={params.LENGTH}
{% endfor %}
_PURGE_UNTIL_EMPTY_RESULT MAX={params.MAX} BDW={bdw if runout_was else '-'}
# Rendered when it runs, so it sees the result of the previous purge.
[gcode_macro _PURGE_UNTIL_EMPTY_STEP]
gcode:
{% if not printer["gcode_macro PURGE_UNTIL_EMPTY"].done %}
{% set length = params.LENGTH|default(0)|float %}
M400
G4 P500
BDP_CHECK_START
BLOBIFIER {'PURGE_LENGTH=%.1f' % length if length > 0 else ''}
M400
BDP_CHECK_STOP
_PURGE_UNTIL_EMPTY_EVAL
{% endif %}
[gcode_macro _PURGE_UNTIL_EMPTY_EVAL]
gcode:
{% set cfg = printer["gcode_macro PURGE_UNTIL_EMPTY"] %}
{% set p = printer.bdpressure_check %}
{% set n = cfg.count + 1 %}
{% if not p.available %}
{action_raise_error("PURGE_UNTIL_EMPTY: lost the bd_pressure load-check reading, stopped. bdwidth runout response may still be off: SET_FILAMENT_SENSOR SENSOR=<bdwidth name> ENABLE=1")}
{% endif %}
{% set empty = p.longest_ms < cfg.empty_ms %}
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE={n}
RESPOND MSG="PURGE_UNTIL_EMPTY purge {n}: pressure longest {p.longest_ms} ms, total {p.above_ms} ms, peak {p.peak} -> {'empty' if empty else 'filament'}"
{% if empty %}
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=done VALUE=1
{% endif %}
[gcode_macro _PURGE_UNTIL_EMPTY_RESULT]
gcode:
{% set cfg = printer["gcode_macro PURGE_UNTIL_EMPTY"] %}
{% if params.BDW|default('-') != '-' %}
SET_FILAMENT_SENSOR SENSOR={params.BDW} ENABLE=1
{% endif %}
{% if cfg.done %}
RESPOND MSG="PURGE_UNTIL_EMPTY: no more filament after {cfg.count} purge{'s' if cfg.count != 1 else ''}. The last bit between the extruder gears and the nozzle is pushed out by the next load."
{% else %}
RESPOND TYPE=error MSG="PURGE_UNTIL_EMPTY: still pressure after {params.MAX} purges. The lane may still be feeding, or the nozzle is blocked - check before running it again."
{% endif %}
+350
View File
@@ -0,0 +1,350 @@
# https://github.com/tanaes/whopping_Voron_mods/blob/main/LEDs/Rainbow_Barf_Logo_LED/Code/stealthburner_led_effects_barf.cfg
# Macros for setting the status leds on the Voron StealthBurner toolhead (or for any neopixel-type leds).
#
# You will need to configure a neopixel (or other addressable led, such as dotstar). See
# https://www.klipper3d.org/Config_Reference.html#neopixel for configuration details.
#####################################
# INSTRUCTIONS #
#####################################
# How to use all this stuff:
#
# 1. Copy this .cfg file into your Klipper config directory and then add [include stealthburner_leds.cfg]
# to the top of your printer.cfg in order for register the LEDs and macros with Klipper.
# 2. Define your LEDs by editing [neopixel sb_leds] below and entering the data pin from your control board
# as well as the color order.
#
# Note: RGB and RGBW are different and must be defined explicitly. RGB and RGBW are also not able to
# be mix-and-matched in the same chain. A separate data line would be needed for proper functioning.
#
# RGBW LEDs will have a visible yellow-ish phosphor section to the chip. If your LEDs do not have
# this yellow portion, you have RGB LEDs.
#
# 3. Save your config and restart Klipper.
#
# Note: We set RED and BLUE to 1.0 to make it easier for users and supporters to detect
# misconfigurations or miswiring. The default color format is for Neopixels with a dedicated
# white LED. On startup, all three SB LEDs should light up.
#
# If you get random colors across your LEDs, change the color_order to GRB and restart. Then
# omit the W for each suggested color_order in the next paragraph.
#
# If you get MAGENTA, your color order is correct. If you get CYAN, you need to use RGBW. If
# you get YELLOW, you need to use BRGW (note that BRG is only supported in the latest Klipper
# version).
#
# 4. Once you have confirmed that the LEDs are set up correctly, you must now decide where you want
# these macros called up...which means adding them to your existing gcode macros. NOTHING will happen
# unless you add the STATUS_????? macros to your existing gcode macros.
#
# Example: add STATUS_LEVELING to the beginning of your QGL gcode macro, and then add STATUS_READY
# to the end of it to set the logo LED and nozzle LEDs back to the `ready` state.
#
# Example: add STATUS_CLEANING to the beginning of your nozzle-cleaning macro, and then STATUS_READY
# to the end of it to return the LEDs back to `ready` state.
#
# 5. Feel free to change colors of each macro, create new ones if you have a need to. The macros provided below
# are just an example of what is possible. If you want to try some more complex animations, you will most
# likely have to use WLED with Moonraker and a small micro-controller (please see the LED thread for help inside
# of the stealthburner_beta channel on Discord).
#
#####################################
# END INSTRUCTRUCTIONS #
#####################################
[neopixel sb_leds]
pin: nhk:PD3
# The pin connected to the neopixel. This parameter must be provided.
chain_count: 10
# The number of Neopixel chips that are "daisy chained" to the
# provided pin. The default is 1 (which indicates only a single
# Neopixel is connected to the pin).
# color_order: GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRBW, GRBW
color_order: GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRB
# Set the pixel order required by the LED hardware. Options are GRB,
# RGB, GRBW, or RGBW. The default is GRB.
initial_RED: 1.0
initial_GREEN: 0.0
initial_BLUE: 1.0
initial_WHITE: 0.0
# Sets the initial LED color of the Neopixel. Each value should be
# between 0.0 and 1.0. The WHITE option is only available on RGBW
# LEDs. The default for each color is 0.#
##########################
# LED Effects Animations #
##########################
##################
## logo effects ##
##################
[led_effect sb_logo_busy]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
layers:
breathing 3 1 top (1,0,0)
[led_effect sb_logo_cleaning]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
layers:
breathing 3 1 top (0.0, 0.02, 0.5)
[led_effect sb_logo_calibrating_z]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
layers:
breathing 3 1 top (0.0, 0.0, 0.35)
[led_effect sb_logo_heating]
leds:
neopixel:sb_leds (2,3,7,8,6,1)
autostart: false
frame_rate: 24
layers:
comet 0.3 0 add (1, 0.18, 0)
[led_effect sb_logo_cooling]
leds:
neopixel:sb_leds (2,3,7,8,6,1)
autostart: false
frame_rate: 24
layers:
comet 0.3 0 add (0, 0, 1)
[led_effect sb_logo_homing]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
layers:
breathing 3 1 top (0.0, 0.6, 0.2)
[led_effect sb_logo_leveling]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
layers:
breathing 3 1 top (0.5, 0.1, 0.4)
[led_effect sb_logo_meshing]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
layers:
breathing 3 1 top (0.2, 1.0, 0.0)
[led_effect sb_logo_printing]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
layers:
gradient 0.3 1 add (0.3, 0.0, 0.0),(0.3, 0.3, 0.0),(0.3, 0.1, 0.0)
[led_effect sb_logo_standby]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
layers:
breathing 3 1 top (0.01, 0.01, 0.01)
[led_effect sb_logo_part_ready]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
layers:
breathing 3 1 top (0.0, 1.0, 0.0)
####################
## nozzle effects ##
####################
[led_effect sb_nozzle_heating]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (9,10)
layers:
breathing 3 1 top (1.0, 0.18, 0.0, 0.0)
[led_effect sb_nozzle_cooling]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (9,10)
layers:
breathing 3 1 top (0.0, 0.0, 1.0, 0.1)
[led_effect sb_nozzle_standby]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (9,10)
layers:
breathing 3 1 top (0.6, 0.0, 0.0, 0.0)
[led_effect sb_nozzle_part_ready]
autostart: false
frame_rate: 24
leds:
neopixel:sb_leds (9,10)
layers:
breathing 3 1 top (0.6, 1.0, 0.0, 0.1)
#####################
## all led effects ##
#####################
[led_effect sb_critical_error]
leds:
neopixel:sb_leds
layers:
strobe 1 1.5 add (1.0, 1.0, 1.0)
breathing 2 0 difference (0.95, 0.0, 0.0)
static 1 0 top (1.0, 0.0, 0.0)
autostart: false
frame_rate: 24
run_on_error: true
[led_effect rainbow]
leds:
neopixel:sb_leds
autostart: true
frame_rate: 24
layers:
gradient 0.3 1 add (0.3, 0.0, 0.0),(0.0, 0.3, 0.0),(0.0, 0.0, 0.3)
#######################
# LED Effects Statics #
#######################
[led_effect set_nozzle_leds]
leds:
neopixel:sb_leds (9,10)
#neopixel:caselight
autostart: false
frame_rate: 24
layers:
# static 0 0 top (0.0, 0.0, 0.0, 1.0)
static 0 0 top (1.0, 1.0, 1.0, 1.0)
[led_effect set_logo_leds]
leds:
neopixel:sb_leds (2,1,3,4,5,6,7,8)
#neopixel:caselight
autostart: false
frame_rate: 24
layers:
static 0 0 top (1.0, 1.0, 1.0)
##############
# The Macros #
##############
[gcode_macro _set_logo_leds_off]
gcode:
SET_LED_EFFECT EFFECT=set_logo_leds STOP=1
[gcode_macro _set_logo_leds_on]
gcode:
SET_LED_EFFECT EFFECT=set_logo_leds
[gcode_macro _set_nozzle_leds_on]
gcode:
SET_LED_EFFECT EFFECT=set_nozzle_leds
[gcode_macro _set_nozzle_leds_off]
gcode:
SET_LED_EFFECT EFFECT=set_nozzle_leds STOP=1
# The _status_* macros only stop the toolhead effects (neopixel:sb_leds), so
# the Box Turtle logo (rainbow_turtle on neopixel:bt_logo) keeps running during
# prints. _status_off still stops everything; _status_ready starts the logo again.
[gcode_macro _status_off]
gcode:
STOP_LED_EFFECTS
[gcode_macro _status_ready]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=rainbow
SET_LED_EFFECT EFFECT=rainbow_turtle
[gcode_macro _status_part_ready]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_nozzle_part_ready
SET_LED_EFFECT EFFECT=sb_logo_part_ready
[gcode_macro _status_busy]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_logo_busy
_set_nozzle_leds_on
[gcode_macro _status_heating]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_logo_heating
SET_LED_EFFECT EFFECT=sb_nozzle_heating
[gcode_macro _status_cooling]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_logo_cooling
SET_LED_EFFECT EFFECT=sb_nozzle_cooling
[gcode_macro _status_leveling]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_logo_leveling
_set_nozzle_leds_on
[gcode_macro _status_homing]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_logo_homing
_set_nozzle_leds_on
[gcode_macro _status_cleaning]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_logo_cleaning
_set_nozzle_leds_on
[gcode_macro _status_meshing]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_logo_meshing
_set_nozzle_leds_on
[gcode_macro _status_calibrating_z]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_logo_calibrating_z
_set_nozzle_leds_on
[gcode_macro _status_printing]
gcode:
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
SET_LED_EFFECT EFFECT=sb_logo_printing
_set_nozzle_leds_on