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:
commit
6dbb4ca3fb
24 files changed
+3906
No files matched your search
@@ -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)}
|
||||
Reference in new issue
Block a user