Compare commits

...
13 Commits
Author SHA1 Message Date
mangavalkandClaude Opus 5.5 87a06f9c30 First layer: AFC fault detection less sensitive
A large, slow first layer tripped AFC's buffer fault detection ("AFC NOT
FEEDING"). _FIRST_LAYER_FAULT_DETECTION ON=1 at the end of PRINT_START
sets first_layer_sensitivity (4: pause after 70 mm instead of 30 mm);
ON=0 restores the [AFC_buffer] config value. The slicer calls ON=0 at
the start of layer 2:
  {if layer_num == 1}_FIRST_LAYER_FAULT_DETECTION ON=0{endif}
_PRINT_CLEANUP calls it too, for prints that end in the first layer.
ON=0 only restores what ON=1 changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 16:47:11 +02:00
mangavalkandClaude Opus 5.5 a63b66a7d8 bdp_guard: new zero at every ARM
With TPU in a nozzle heating to 150C the pressure kept changing for
minutes (+43, then -56 as it oozed), faster than the sensor's own
baseline update, and the guard stopped the printer at the start of
z_check and home_xy with the nozzle in the air. Every ARM (also a phase
switch) now writes reset_probe (register 52) and waits 0.2 s without
reading, so only force during the guarded moves counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:34:26 +02:00
mangavalkandClaude Opus 5.5 29604b2f1e PA calibration: wider PA range for TPU/TPE
PA_CALIBRATE takes PA_STEP (default 0.002, 0-0.098 over 50 lines).
_PA_CFG.material_pa_step uses 0.004 (0-0.196) for tpu/tpe/flex; soft TPU
measured 0.094, close to the old end. The "no result" rejection uses the
end of the range actually tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:29:02 +02:00
mangavalkandClaude Opus 5.5 8a5f6b6dd9 PA calibration: lower test flow for TPU/TPE
_PA_CFG.material_max_volumetric lowers MAX_VOLUMETRIC for materials that
cannot extrude 25 mm3/s (tpu, tpe, flex: 8). The calibration line shows
the flow used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:15:03 +02:00
mangavalkandClaude Opus 5.5 93ae8795b8 PA calibration reads Spoolman first; guard resets a sensor left in PA mode
- LANE_CALIBRATE_PA: when a calibration will run, assign the lane's spool
  again (SET_SPOOL_ID) so AFC reads the Spoolman temperature now; AFC
  only reads it at spool assignment and Klipper start (TPU calibrated at
  210C after Spoolman was set to 230C). The calibration moved to
  _LANE_CALIBRATE_PA_RUN, rendered after the refresh.
- bdp_guard: an emergency stop during PA_CALIBRATE leaves the sensor in
  PA mode (FIRMWARE_RESTART does not reset it). When arming, read the
  mode register and switch the sensor back to probe mode with a fresh
  baseline before watching it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:04:40 +02:00
mangavalkandClaude Opus 5.5 d49e65c0bb Flexible lanes load slower; purge at the previous filament's temperature
- flex_lanes.cfg: a 5 s poller sets SET_LONG_MOVE_SPEED to 50 mm/s (AFC's
  minimum) for lanes whose material is TPU/TPE/flex and back to the
  configured long_moves_speed otherwise; reapplied after a restart.
- BLOBIFIER_CHECKED: remembers the temperature of the filament in the
  nozzle (saved nozzle_last_temp). When it is more than flush_margin
  (10C) above the new filament's, the purge runs at that temperature and
  the nozzle cools back afterwards, so the leftover melts out (seen: ASA
  leftover with TPU loaded at 210C).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:59:27 +02:00
mangavalkandClaude Opus 5.5 464a7c1046 CLAUDE.md: purge check peaks up to 246 with ASA at 250-260C
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:20:02 +02:00
mangavalkandClaude Opus 5.5 0b1b2a6947 Purge check: accept load-check firmware v2; chamber load levels first
- bdpressure_check.py accepted only extension version 1. The sensor now
  runs firmware version 2 (bytes 54-65 unchanged, diagnostics added), so
  every purge check reported bd_pressure as not answering. Accept 1 and 2.
- _LOAD_FOR_CHAMBER: the Blobifier only purges with the QGL applied, so
  the load before the chamber soak skipped its purge (0.0 mm extruded).
  Level the gantry and home Z first when the QGL is not applied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 12:12:25 +02:00
mangavalkandClaude Opus 5.5 b9d2f3a062 Nozzle guard in stop mode (brush log-only), wait for the nozzle before HOME
Test print start: guard peaks 1-8 in every phase except the brush scrub
(peak 47, 4 reads above 30 in a row), 0 I2C errors; a light finger push
gives 41. Stop mode for all phases, the brush stays log-only.

The contact homing in HOME restarted 6 times (sample spread too large)
while the nozzle was still heating to 150C; PRINT_WARMUP now waits for a
steady contact temperature first, as PRINT_START does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:25:28 +02:00
mangavalkandClaude Opus 5.5 a16b57a0a9 Move bd_pressure.cfg and TEST_SPEED into the repo
All macros now come from this repo. bd_pressure.cfg ([bdpressure bd_pa]
and the vendor PA_CALIBRATE macros) and Macros/TEST_SPEED.cfg were loose
files in ~/printer_data/config.

PA_CALIBRATE no longer sets Z back to z_current at the end: it was read
before the test ran, so a Z hop in the G28 XY (Beacon home_z_hop) would
leave Klipper believing the nozzle is lower than it is. Z is never switched
off or moved by the test, so it keeps its position without that line.
The ACC_WALL bug (read from ACCEL_TO_DECEL, always 4000) is documented,
not fixed: the cached PA values were measured at 4000.

printer.cfg must drop [include bd_pressure.cfg] and Macros/macros.cfg its
TEST_SPEED include, else the old copies (included later) still win.

Reference values from the load-check-afc branch and from the guard and
Z check tests added to CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:11:02 +02:00
mangavalkandClaude Opus 5.5 8eef0ee298 bdp_guard: one lock for all I2C transfers of the bd_pressure
The guard's timer read the sensor while G28 ran; bdpressure.py writes the
probe mode at every homing move, and Klipper allows only one I2C request
at a time per device: KeyError ('i2c_response', 1), Klipper shut down
during HOME. The guard now wraps the sensor's i2c in a reactor mutex at
connect; bdpressure and bdpressure_check (now looks up sensor.i2c at
every use) go through it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 23:06:49 +02:00
mangavalkandClaude Opus 5.5 61fa8acb05 Print start: contact homing, nozzle guard, Z check, purge before chamber heat
- HOME always homes Z with Beacon contact and recalibrates, also when the
  printer is already homed (home_method_when_homed = proximity used the
  model of an earlier print without touching the bed). PRINT_START homes
  Z again after QGL, before the brush takes the nozzle below Z0.
- _Z_CHECK: after the soak, compare Klipper's Z with a BEACON_QUERY
  reading at Z3 over the bed centre and stop the print above 1 mm.
  Warns and continues when BEACON_QUERY leaves no new
  printer.beacon.last_sample.
- Nozzle guard (extras/bdp_guard.py, print/nozzle_guard.cfg): reads the
  bd_pressure force over I2C every 20 ms while armed and, in stop mode,
  shuts Klipper down on a touch. Armed for HOME X/Y, QGL, Z homing, brush,
  mesh and the Z check; starts in log mode to collect peaks per phase.
- Chamber prints: load and purge the first tool before the chamber heats
  (_LOAD_FOR_CHAMBER replaces _UNLOAD_FOR_CHAMBER). A cut unload leaves a
  piece of PLA in the extruder that softened in the hot chamber and jammed
  the ASA load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:39:26 +02:00
mangavalkandClaude Opus 5.5 2100857846 Chamber prints: unload a lane that is not the first tool before the soak
With the chamber heated, a filament left in the toolhead path softens in
the PTFE tube (PLA loaded, ASA print). After the first HOME, PRINT_WARMUP
unloads the loaded lane when it is not mapped to the print's first tool
(PRINT_START would swap it anyway). A failed unload stops the print
before the chamber heats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:56:30 +02:00
13 changed files with 1007 additions and 26 deletions

No files matched your search

+14 -1
View File
@@ -70,6 +70,11 @@ detection, `filament_error_sensitivity`), iHeater chamber heater.
not `action_raise_error` / `RESPOND TYPE=error`, so they also show on the
display. `action_raise_error` aborts while rendering, so an `M117` in the
same macro would never run. No double quotes in `MSG`/`DISPLAY`.
- Nozzle guard (`print/nozzle_guard.cfg`, `extras/bdp_guard.py`): wrap only
moves where nothing may touch the nozzle in `_GUARD_ON PHASE=x` /
`_GUARD_OFF`. Never leave it on across contact homing, extrusion, purges,
the Blobifier park or AFC cuts and loads: in `mode: stop` that is an
instant shutdown.
- After a refactor run `python tools/effective_config.py HEAD` and check that
only the intended sections changed.
- Render-test macros with Jinja2 using Klipper's delimiters
@@ -89,7 +94,15 @@ detection, `filament_error_sensitivity`), iHeater chamber heater.
## Reference values
- Purge check (150 mm Blobifier purge): good load = longest pressure
13-18 s, peak 64-181; empty nozzle about 30 ms.
13-18 s, peak 64-246 (ASA at 250-260C peaks highest); empty nozzle
about 30 ms. A real failed load in a
print start (AFC reported the lane loaded, nothing reached the nozzle):
longest 1915 ms, peak 45, caught by the 5000 ms limit.
- Nozzle guard (bdp_guard): peaks 1-8 in home_xy, QGL, Z homing, mesh and
the Z check; the brush scrub 47 (4 reads above 30 in a row, so it runs
in log mode); a light finger push on the cold nozzle gives 41
(threshold 30).
- Z check after a contact homing: Beacon vs Klipper Z about 0.06 mm.
- XY drift check: endstop noise about 0.01-0.02 mm; limit 0.1 mm (one motor
skipping one cycle = 0.4 mm shows as 0.2 mm in X and Y).
+11 -4
View File
@@ -21,10 +21,12 @@ everything below, grouped by topic. Comment out what you do not use there.
| `blobifier.cfg` | `BLOBIFIER`, `BLOBIFIER_CLEAN`, `BLOBIFIER_PARK`, `BLOBIFIER_SHAKE_BUCKET`, ...: purge bucket and nozzle brush for AFC (Turtleblobifier, heavily modified) | Blobifier hardware, AFC |
| `blobifier_hw.cfg` | servo and bucket switch pins | — |
| **print/** | | |
| `print_start_end.cfg` | `PRINT_WARMUP`, `PRINT_START`, `PRINT_END` | Beacon, AFC, Blobifier, `HEAT_SOAK`, iHeater, bd_pressure PA macros (bdwidth is no longer enabled while printing) |
| `print_start_end.cfg` | `PRINT_WARMUP`, `PRINT_START`, `PRINT_END`; `_FIRST_LAYER_FAULT_DETECTION`: AFC buffer fault detection less sensitive during the first layer (needs one line in the slicer's layer change G-code, see the macro) | Beacon, AFC, Blobifier, `HEAT_SOAK`, iHeater, bd_pressure PA macros (bdwidth is no longer enabled while printing) |
| `pause_resume.cfg` | `PAUSE`, `RESUME`, `CANCEL_PRINT`, `M600` (RESUME also handles a running `HEAT_SOAK`, and after a PAUSE purges 40 mm with `BLOBIFIER_CHECKED` first: a failed check keeps the print paused, `RESUME PURGE=0` skips the purge; CANCEL_PRINT runs the same cleanup as `PRINT_END`) | `heating/heat_soak.cfg`, `leds/`, `blobifier/`, `afc/purge_check.cfg` |
| `filament.cfg` | `LOAD_FILAMENT`, `UNLOAD_FILAMENT` | — |
| `homing.cfg` | `HOME` | — |
| `homing.cfg` | `HOME`: X/Y, then Z always with Beacon contact (also when already homed); `_Z_CHECK`: stops the print when Klipper's Z and the Beacon reading differ (run in `PRINT_START` after the soak) | Beacon |
| `nozzle_guard.cfg` | `[bdp_guard]`, `_GUARD_ON PHASE=<name>`, `_GUARD_OFF`: bd_pressure watches the nozzle force during print-start moves where nothing should touch it (QGL, Z homing, mesh); `mode: stop` shuts Klipper down on a touch (the brush runs log-only), `mode: log` only reports the peak per phase | `extras/bdp_guard.py` + bd_pressure load-check firmware |
| `test_speed.cfg` | `TEST_SPEED`: skipped-steps test for speed and acceleration | — |
| `idle_timeout.cfg` | `[idle_timeout]`: heaters, chamber heater and motors off | iHeater macros |
| **heating/** | | |
| `heat_soak.cfg` | `HEAT_SOAK`, `STOP_HEAT_SOAK`, `CANCEL_HEAT_SOAK`, `HEAT_SOAK_RESUME`, `RESUME` | — |
@@ -35,10 +37,12 @@ everything below, grouped by topic. Comment out what you do not use there.
| **afc/** | | |
| `xy_safety_check.cfg` | `AFC_XY_SAFETY_CHECK`, `_AFC_STORE_HOME_REFERENCE`, `AFC_XY_RESUME`: lost-step check around tool changes | `extras/xy_drift.py` (CoreXY) |
| `lane_tools.cfg` | `LANE_FEED_UNTIL_HUB_OPEN`, `LANE_FEED_ABORT` | AFC |
| `purge_check.cfg` | `BLOBIFIER_CHECKED`: checks every AFC purge with bd_pressure (and optionally bdwidth motion); a bd_pressure that does not answer fails the check, `PURGE_CHECK_IGNORE_SENSOR` lets the current print finish without it | Blobifier, AFC, `extras/bdpressure_check.py` + bd_pressure load-check firmware, `extras/bdwidth_status.py` |
| `purge_check.cfg` | `BLOBIFIER_CHECKED`: checks every AFC purge with bd_pressure (and optionally bdwidth motion); a bd_pressure that does not answer fails the check, `PURGE_CHECK_IGNORE_SENSOR` lets the current print finish without it; when the previous filament (saved `nozzle_last_temp`) needs more than 10°C above the new one, the purge runs at the previous filament's temperature and the nozzle cools back afterwards, so its leftover melts out | Blobifier, AFC, `extras/bdpressure_check.py` + bd_pressure load-check firmware, `extras/bdwidth_status.py` |
| `flex_lanes.cfg` | Lanes holding TPU/TPE (material from Spoolman) get slower long moves (50 mm/s, AFC's minimum) through the tube; back to the configured speed when the lane holds something else. Polls every 5 s, reapplies after a restart. The last stretch into the extruder (`tool_load_speed`) stays as configured | AFC |
| `purge_until_empty.cfg` | `PURGE_UNTIL_EMPTY`: purges a snapped filament out until there is no more nozzle pressure | same as `purge_check.cfg` |
| **pressure_advance/** | | |
| `bd_pa_spoolman.cfg` + `bd_pa_*.sh` | `LANE_CALIBRATE_PA`, `PA_CALIBRATE_ALL_LANES`, `LANE_RECAL_PA`, `SET_SPOOL_ID`, `PA_STATUS`: pressure advance per lane/spool, cached locally and in Spoolman; a calibration with no result, a pause during the test or too few/stuck sensor readings is rejected and not saved | bd_pressure, AFC, Spoolman, `gcode_shell_command` |
| `bd_pressure.cfg` | `[bdpressure bd_pa]` (I2C on the Nitehawk) and the vendor macros `PA_CALIBRATE`, `PA_E`, `PA_RESET`, `PA_STATE`; `PA_CALIBRATE` no longer sets Z back to a value read before the test (wrong after a Z hop in `G28 XY`) | `bdpressure.py` from the bd_pressure repo |
| `bd_pa_spoolman.cfg` + `bd_pa_*.sh` | `LANE_CALIBRATE_PA`, `PA_CALIBRATE_ALL_LANES`, `LANE_RECAL_PA`, `SET_SPOOL_ID`, `PA_STATUS`: pressure advance per lane/spool, cached locally and in Spoolman; before a calibration the lane's spool is assigned again so a temperature just changed in Spoolman is used; flexible materials (TPU/TPE) calibrate at a lower flow (8 mm³/s) and over a wider PA range (0–0.196 in 0.004 steps instead of 0–0.098); a calibration with no result, a pause during the test or too few/stuck sensor readings is rejected and not saved | bd_pressure, AFC, Spoolman, `gcode_shell_command` |
| **spoolman/** | | |
| `active_spool.cfg` | `SET_ACTIVE_SPOOL`, `CLEAR_ACTIVE_SPOOL` | Spoolman in Moonraker |
| **leds/** | | |
@@ -53,6 +57,7 @@ everything below, grouped by topic. Comment out what you do not use there.
| `xy_drift.py` | `AFC_XY_SAFETY_CHECK`: measures lost XY steps from the stepper step counts at the endstop triggers (CoreXY). The toolhead position after homing cannot show lost steps. |
| `bdwidth_status.py` | exposes bdwidth's motion counter and width to macros (`present: False` with neutral values when no bdwidth is configured, so the macros also work without it), and keeps a USB dropout of bdwidth (port: usb) from shutting down Klipper: the read is retried every 10 s and the port reopened |
| `bdpressure_check.py` | bd_pressure load check (`BDP_CHECK_START/STOP/QUERY`). Needs the load-check firmware from the bd_pressure `load-check-extension` branch; on stock firmware it reports "not available" and does nothing. Also reports how many readings the last `PA_CALIBRATE` got (`pa_points`, `pa_distinct`), used to reject runs with a silent or stuck sensor. |
| `bdp_guard.py` | nozzle collision guard: while armed (`BDP_GUARD_ARM/DISARM/QUERY`) reads the bd_pressure force every 20 ms over I2C and, in stop mode, shuts Klipper down when it stays above the threshold. Reports reads, peak and errors per phase in the console and klippy.log. When it arms and finds the sensor still in PA mode (an emergency stop during `PA_CALIBRATE`; `FIRMWARE_RESTART` does not reset the sensor), it switches it to probe mode and takes a new zero first. Puts a lock on the sensor's I2C connection that `bdpressure.py` and `bdpressure_check.py` also go through (Klipper allows one I2C request at a time per device). Needs the load-check firmware. |
### Scripts (`scripts/`)
@@ -158,6 +163,8 @@ original licenses apply to those parts:
| `heating/heat_soak.cfg` | community `HEAT_SOAK` macro |
| `heating/chamber_iheater.cfg` | iHeater vendor configuration |
| `heating/filament_drying.cfg` | community filament drying macro |
| `pressure_advance/bd_pressure.cfg` | [bd_pressure](https://github.com/markniu/bd_pressure) `klipper/bd_pressure.cfg` |
| `print/test_speed.cfg` | [Ellis' TEST_SPEED](https://ellis3dp.com/Print-Tuning-Guide/articles/determining_max_speeds_accels.html) |
| `print/pause_resume.cfg`, `print/filament.cfg` | [KlipperScreen recommended macros](https://klipperscreen.readthedocs.io/en/latest/macros/#recommended) |
| `scripts/` | [Voron docs: automating MCU updates](https://docs.vorondesign.com/community/howto/drachenkatze/automating_klipper_mcu_updates.html), [Esoterical's CANBus guide](https://canbus.esoterical.online/) |
+283
View File
@@ -0,0 +1,283 @@
# Nozzle collision guard with bd_pressure (load-check firmware extension).
#
# The bd_pressure strain gauge sees the force on the nozzle. The load-check
# firmware keeps the current force minus its baseline in register 64-65
# (chk_delta), also outside a load check; it is the same value the endstop
# output switches on (|delta| >= thrhold). While the guard is armed this
# module reads it every INTERVAL seconds. In stop mode, SAMPLES reads in a
# row at or above THRESHOLD shut Klipper down at once (like M112, without
# waiting for the running macro to finish).
#
# Every ARM (also a phase switch) first sets a new zero (register 52), so
# slow pressure changes before the guarded moves do not count (TPU in a
# nozzle that heats up keeps changing pressure for minutes).
#
# Arm it only around moves where the nozzle must not touch anything: not
# around contact homing, extrusion, purges or AFC cuts and loads, which
# press on the nozzle on purpose. ARM and DISARM act immediately, not in
# the move queue: put an M400 before them (the _GUARD_ON/_GUARD_OFF macros
# do this).
#
# Install: copy to ~/klipper/klippy/extras/bdp_guard.py and add
#
# [bdp_guard]
# sensor: bd_pa # name after "bdpressure" in [bdpressure bd_pa]
# mode: log # log: only measure; stop: shut down on a touch
# threshold: 30 # |delta| that counts as a touch
# samples: 3 # reads in a row at or above threshold
# interval: 0.02 # seconds between reads while armed
# timeout: 600 # disarm by itself when a phase runs this long
# # (a macro that failed before its DISARM)
#
# Commands:
# BDP_GUARD_ARM PHASE=<name> [MODE=log|stop] start (or switch phase)
# BDP_GUARD_DISARM stop and report the phase
# BDP_GUARD_QUERY report without stopping
#
# Each phase reports (console and klippy.log, "bdp_guard:") the reads, the
# peak |delta|, the longest run of reads at or above threshold and the
# read errors, so a log-mode print start shows what each phase normally
# measures before stop mode is switched on.
#
# Status (printer.bdp_guard): armed, mode, phase, threshold, samples, peak,
# longest_run, reads, errors, available
import logging
REG_MODE = 48 # status_clk: 7 = PA mode, 2 = probe (endstop) mode
REG_RESET_PROBE = 52 # set_normal: write 1 = take the current reading as zero
MODE_PA = 7
REG_EXT = 54 # extension marker 0xBD + version
REG_DELTA = 64 # chk_delta, int16 little-endian
EXT_MAGIC = 0xBD
MODES = ('log', 'stop')
# Klipper allows only one I2C request at a time per device: a second one
# while the first waits for its answer raises KeyError ('i2c_response', oid)
# and shuts Klipper down. The guard reads from a timer while G-code runs,
# and bdpressure.py writes the probe mode at every homing move (seen: G28
# during an armed guard), so all transfers of the sensor take this lock.
# The guard puts it on the sensor object at connect; bdpressure and
# bdpressure_check use sensor.i2c and so get it too.
class LockedI2C:
def __init__(self, i2c, mutex):
self._i2c = i2c
self._mutex = mutex
def i2c_read(self, *args, **kwargs):
with self._mutex:
return self._i2c.i2c_read(*args, **kwargs)
def i2c_write(self, *args, **kwargs):
with self._mutex:
return self._i2c.i2c_write(*args, **kwargs)
def i2c_write_wait_ack(self, *args, **kwargs):
with self._mutex:
return self._i2c.i2c_write_wait_ack(*args, **kwargs)
def i2c_transfer(self, *args, **kwargs):
with self._mutex:
return self._i2c.i2c_transfer(*args, **kwargs)
def __getattr__(self, name):
return getattr(self._i2c, name)
class BDPGuard:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.sensor_name = 'bdpressure ' + config.get('sensor', 'bd_pa')
self.mode = config.getchoice('mode', {m: m for m in MODES}, 'log')
self.threshold = config.getint('threshold', 30, minval=1)
self.samples = config.getint('samples', 3, minval=1)
self.interval = config.getfloat('interval', 0.02, minval=0.005)
self.timeout = config.getfloat('timeout', 600., minval=10.)
self.armed_until = 0.
self.sensor = self.i2c = None
self.available = False
self.timer = None
self.zeroing = False
self.phase_mode = self.mode
self._reset_phase('')
self.printer.register_event_handler('klippy:connect',
self._handle_connect)
self.printer.register_event_handler('klippy:shutdown',
self._handle_shutdown)
gcode = self.printer.lookup_object('gcode')
gcode.register_command('BDP_GUARD_ARM', self.cmd_ARM,
desc="Arm the bd_pressure nozzle collision guard")
gcode.register_command('BDP_GUARD_DISARM', self.cmd_DISARM,
desc="Disarm the bd_pressure nozzle collision guard")
gcode.register_command('BDP_GUARD_QUERY', self.cmd_QUERY,
desc="Report the bd_pressure guard values")
def _handle_connect(self):
self.sensor = self.printer.lookup_object(self.sensor_name, None)
i2c = getattr(self.sensor, 'i2c', None)
if i2c is not None and not isinstance(i2c, LockedI2C):
# Every user of the sensor (bdpressure, bdpressure_check, this
# guard) now goes through one lock, see LockedI2C
i2c = LockedI2C(i2c, self.reactor.mutex())
self.sensor.i2c = i2c
self.i2c = i2c
def _handle_shutdown(self):
self._stop_timer()
def _reset_phase(self, phase):
self.phase = phase
self.peak = self.longest_run = self.run = 0
self.reads = self.errors = 0
def _check_available(self):
if self.i2c is None:
return False
try:
data = bytearray(self.i2c.i2c_read([REG_EXT], 2)['response'])
except Exception:
logging.exception("bdp_guard: reading the extension marker failed")
return False
return len(data) == 2 and data[0] == EXT_MAGIC and data[1] >= 1
def _ensure_probe_mode(self, gcmd):
# An emergency stop during PA_CALIBRATE leaves the sensor in PA mode
# (FIRMWARE_RESTART does not reset it); bdpressure.py only switches
# it back at the next homing move, after the guard is armed. In PA
# mode the delta means nothing and could trip the guard. Register 48
# is the mode: 7 = PA, 2 = probe.
if getattr(self.sensor, 'last_state', 0):
return # a calibration runs, leave the sensor alone
try:
mode = bytearray(self.i2c.i2c_read([REG_MODE], 1)['response'])
if len(mode) != 1 or mode[0] != MODE_PA:
return
self.sensor.set_probe_mode()
# new samples at the probe sample rate, then a fresh baseline
self.reactor.pause(self.reactor.monotonic() + 0.5)
self.i2c.i2c_write([REG_RESET_PROBE, 1])
self.reactor.pause(self.reactor.monotonic() + 0.3)
except Exception:
logging.exception("bdp_guard: switching the sensor to probe mode failed")
return
msg = ("BDP_GUARD: bd_pressure was still in PA mode (stopped "
"calibration?), switched to probe mode")
logging.info(msg)
gcmd.respond_info(msg)
def _rezero(self):
# A fresh zero at every ARM: only force that appears during the
# guarded moves counts, not slow changes before them. Seen with TPU
# in a nozzle heating to 150C: the pressure kept rising (+43) or
# falling as it oozed (-56) for minutes, faster than the sensor's
# own baseline update (only when steady, about every 7 s), and
# stopped the printer at the start of a phase with the nozzle in
# the air. ARM comes after M400, with the nozzle clear of
# everything, so this does not hide a real touch.
self.zeroing = True
try:
self.i2c.i2c_write([REG_RESET_PROBE, 1])
self.reactor.pause(self.reactor.monotonic() + 0.2)
except Exception:
logging.exception("bdp_guard: setting a new zero failed")
self.zeroing = False
def _stop_timer(self):
if self.timer is not None:
self.reactor.unregister_timer(self.timer)
self.timer = None
def _poll(self, eventtime):
# Never raise here: an exception in a timer shuts Klipper down.
try:
if self.printer.is_shutdown():
return self.reactor.NEVER
if eventtime > self.armed_until:
logging.info("bdp_guard: %s armed longer than %.0f s, "
"disarmed (%d reads, peak %d)", self.phase,
self.timeout, self.reads, self.peak)
self._stop_timer()
self.phase = ''
return self.reactor.NEVER
# PA mode (SET_BDPRESSURE START): the delta means nothing then;
# while ARM sets a new zero the readings are in between
if self.zeroing or getattr(self.sensor, 'last_state', 0):
return eventtime + self.interval
data = bytearray(self.i2c.i2c_read([REG_DELTA], 2)['response'])
if len(data) != 2:
self.errors += 1
return eventtime + self.interval
delta = data[0] | (data[1] << 8)
if delta & 0x8000:
delta -= 0x10000
mag = abs(delta)
self.reads += 1
self.peak = max(self.peak, mag)
self.run = self.run + 1 if mag >= self.threshold else 0
self.longest_run = max(self.longest_run, self.run)
if self.run >= self.samples and self.phase_mode == 'stop':
# klippy:shutdown unregisters this timer
self.printer.invoke_shutdown(
"bd_pressure guard: nozzle touched something during %s "
"(delta %d, threshold %d)"
% (self.phase or 'an armed move', delta, self.threshold))
return self.reactor.NEVER
except Exception:
self.errors += 1
if self.errors == 1: # 50 reads/s: only the first per phase
logging.exception("bdp_guard: read failed")
return eventtime + self.interval
def _report(self, gcmd, prefix):
msg = ("%s %s (%s): %d reads, peak %d, longest run >= %d: %d reads, "
"%d errors" % (prefix, self.phase or '-', self.phase_mode,
self.reads, self.peak, self.threshold,
self.longest_run, self.errors))
logging.info("bdp_guard: %s", msg)
gcmd.respond_info(msg)
def cmd_ARM(self, gcmd):
phase = gcmd.get('PHASE', 'armed')
mode = gcmd.get('MODE', self.mode).lower()
if mode not in MODES:
raise gcmd.error("BDP_GUARD_ARM: MODE must be log or stop")
if self.timer is not None:
self._report(gcmd, "BDP_GUARD")
else:
self.available = self._check_available()
if not self.available:
gcmd.respond_info("BDP_GUARD_ARM: bd_pressure load-check "
"extension not found, guard not armed")
return
self._ensure_probe_mode(gcmd)
self._rezero()
self.phase_mode = mode
self._reset_phase(phase)
self.armed_until = self.reactor.monotonic() + self.timeout
if self.timer is None:
self.timer = self.reactor.register_timer(
self._poll, self.reactor.monotonic() + self.interval)
def cmd_DISARM(self, gcmd):
if self.timer is None:
return
self._stop_timer()
self._report(gcmd, "BDP_GUARD")
self.phase = ''
def cmd_QUERY(self, gcmd):
self._report(gcmd, "BDP_GUARD_QUERY" + (" armed" if self.timer
else " not armed"))
def get_status(self, eventtime):
return {'armed': self.timer is not None, 'mode': self.phase_mode,
'phase': self.phase, 'threshold': self.threshold,
'samples': self.samples, 'peak': self.peak,
'longest_run': self.longest_run, 'reads': self.reads,
'errors': self.errors, 'available': self.available}
def load_config(config):
return BDPGuard(config)
+16 -8
View File
@@ -2,7 +2,7 @@
#
# Uses the I2C connection the existing [bdpressure <name>] section already
# set up. Before writing anything it reads the extension marker (bytes
# 54-55 = 0xBD 0x01). On stock firmware the marker is missing and every
# 54-55 = 0xBD + version 1 or 2). On stock firmware the marker is missing and every
# command only reports "not available": nothing is written, because
# writing past the stock register table would land in the sensor's RAM.
#
@@ -27,15 +27,18 @@
REG_EXT = 54 # first byte of the extension
REG_CTRL = 56
EXT_LEN = 12 # bytes 54-65
EXT_MAGIC = (0xBD, 0x01)
EXT_LEN = 12 # bytes 54-65, the same in version 1 and 2
EXT_MAGIC = 0xBD
# Version 2 (load-check firmware 20261015 and later) only adds diagnostics
# at 66-71. Accepting only version 1 made the purge check report the sensor
# as not answering after the firmware update.
EXT_VERSIONS = (1, 2)
class BDPressureCheck:
def __init__(self, config):
self.printer = config.get_printer()
self.sensor_name = 'bdpressure ' + config.get('sensor', 'bd_pa')
self.i2c = None
self.sensor = None
self.status = {'available': False, 'running': False, 'peak': 0,
'above_ms': 0, 'longest_ms': 0, 'delta': 0}
@@ -50,16 +53,21 @@ class BDPressureCheck:
desc="Read the bd_pressure load check values")
def _handle_connect(self):
sensor = self.printer.lookup_object(self.sensor_name)
self.sensor = sensor
self.i2c = getattr(sensor, 'i2c', None)
self.sensor = self.printer.lookup_object(self.sensor_name)
@property
def i2c(self):
# Looked up at every use: bdp_guard replaces the sensor's i2c with a
# locked wrapper at connect, possibly after this module's connect.
return getattr(self.sensor, 'i2c', None)
def _read(self):
if self.i2c is None:
self.status['available'] = False
return False
data = bytearray(self.i2c.i2c_read([REG_EXT], EXT_LEN)['response'])
if len(data) < EXT_LEN or tuple(data[0:2]) != EXT_MAGIC:
if (len(data) < EXT_LEN or data[0] != EXT_MAGIC
or data[1] not in EXT_VERSIONS):
self.status['available'] = False
return False
u16 = lambda i: data[i] | (data[i + 1] << 8)
+54
View File
@@ -0,0 +1,54 @@
# Flexible filament (TPU, TPE): slower long moves for the lanes holding it.
#
# Soft filament can buckle in the long tube at the normal 100-200 mm/s.
# A poller checks the material of every lane (from Spoolman via AFC) every
# 5 s and sets the lane's long-move speed: flex_speed for a flexible
# material, the configured long_moves_speed again when the lane holds
# something else. Only changes are sent. SET_LONG_MOVE_SPEED is runtime
# only; after a Klipper restart the poller applies it again.
#
# Not covered: the last stretch into the extruder (tool_load_speed in
# AFC/AFC_Hardware.cfg, 25 mm/s) has no runtime command and is the same
# for every lane. The purge after the load is already slow (2.5 mm/s).
[gcode_macro _FLEX_LANES_CFG]
variable_materials: ["tpu", "tpe", "flex"] # material containing one of these (any case) is flexible
variable_flex_speed: 50 # mm/s; AFC's minimum for SET_LONG_MOVE_SPEED is 50
variable_slowed: "" # runtime: comma list of lanes now at flex_speed
gcode:
[delayed_gcode _FLEX_LANES_POLL]
initial_duration: 10
gcode:
{% set cfg = printer["gcode_macro _FLEX_LANES_CFG"] %}
{% if 'AFC' in printer %}
{% set settings = printer.configfile.settings %}
{% set default_speed = settings.afc.long_moves_speed|default(150) if 'afc' in settings else 150 %}
{% set was = cfg.slowed.split(',')|reject('equalto', '')|list %}
{% set now = [] %}
{% for lane in printer.AFC.lanes %}
{% set key = 'AFC_stepper ' ~ lane %}
{% set mat = (printer[key].material if key in printer else '')|default('', true)|string %}
{% set ns = namespace(flex=false) %}
{% for m in cfg.materials %}
{% if m in mat|lower %}
{% set ns.flex = true %}
{% endif %}
{% endfor %}
{% if ns.flex %}
{% set _ = now.append(lane) %}
{% if lane not in was %}
SET_LONG_MOVE_SPEED LANE={lane} FWD_SPEED={cfg.flex_speed}
RESPOND MSG="{lane} holds {mat}: long moves at {cfg.flex_speed} mm/s (flexible)"
{% endif %}
{% elif lane in was %}
{% set speed = settings[key|lower].long_moves_speed|default(default_speed) if (key|lower) in settings else default_speed %}
SET_LONG_MOVE_SPEED LANE={lane} FWD_SPEED={speed}
RESPOND MSG="{lane} holds {mat or 'nothing'}: long moves back at {speed} mm/s"
{% endif %}
{% endfor %}
{% if now|join(',') != was|join(',') %}
SET_GCODE_VARIABLE MACRO=_FLEX_LANES_CFG VARIABLE=slowed VALUE='"{now|join(',')}"'
{% endif %}
{% endif %}
UPDATE_DELAYED_GCODE ID=_FLEX_LANES_POLL DURATION=5
+41
View File
@@ -54,6 +54,14 @@ variable_start_counts: 0
variable_start_e: 0.0
variable_last_failed: False # last check failed in enforce mode (read by _RESUME_PURGE_VERIFY)
variable_ignore_missing_sensor: False # set by PURGE_CHECK_IGNORE_SENSOR, cleared at print end
# Hotter purge: the leftover of the previous filament (the piece below the
# cutter) has to melt to be pushed out. When the filament that last went
# through the nozzle (saved variable nozzle_last_temp) needs more than the
# new one plus flush_margin, the purge runs at that temperature and the
# nozzle cools back to the new filament's afterwards (seen: ASA leftover,
# TPU loaded at 210C, purge only worked after heating by hand).
variable_flush_margin: 10 # C; smaller differences purge at the new temperature
variable_flush_restore: 0 # runtime: temperature to cool back to after a hot purge
gcode:
[gcode_macro BLOBIFIER_CHECKED]
@@ -77,6 +85,7 @@ gcode:
# 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 = has_bdw and (bdw_on or not cfg.motion_check) %}
_PURGE_FLUSH_HEAT
{% if cfg.mode == "off" %}
BLOBIFIER {rawparams}
{% else %}
@@ -107,9 +116,41 @@ gcode:
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE={runout_was}
{% endif %}
{% endif %}
_PURGE_FLUSH_DONE
# Separate macros so the status is read when they run, not when
# BLOBIFIER_CHECKED is rendered
[gcode_macro _PURGE_FLUSH_HEAT]
description: Helper for BLOBIFIER_CHECKED: heat to the previous filament's temperature when it is hotter than the new one
gcode:
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
{% set old = printer.save_variables.variables.nozzle_last_temp|default(0)|int %}
{% set new = printer.extruder.target|int %}
{% set top = printer.configfile.settings.extruder.max_temp|int - 15 %}
{% set hot = [old, top]|min %}
{% if new > 0 and hot > new + cfg.flush_margin %}
RESPOND MSG="Purge at {hot}C: the previous filament needs {old}C, back to {new}C after the purge"
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=flush_restore VALUE={new}
M104 S{hot}
TEMPERATURE_WAIT SENSOR=extruder MINIMUM={hot - 2}
{% else %}
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=flush_restore VALUE=0
{% endif %}
[gcode_macro _PURGE_FLUSH_DONE]
description: Helper for BLOBIFIER_CHECKED: cool back after a hot purge and remember the new filament's temperature
gcode:
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
{% set new = cfg.flush_restore|int if cfg.flush_restore|int > 0 else printer.extruder.target|int %}
{% if cfg.flush_restore|int > 0 %}
M104 S{new}
TEMPERATURE_WAIT SENSOR=extruder MAXIMUM={new + 3}
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=flush_restore VALUE=0
{% endif %}
{% if new > 0 %}
SAVE_VARIABLE VARIABLE=nozzle_last_temp VALUE={new}
{% endif %}
[gcode_macro _PURGE_CHECK_START]
gcode:
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=start_counts VALUE={printer.bdwidth_status.total_move}
+4
View File
@@ -20,9 +20,11 @@
# Printing
[include print/pause_resume.cfg]
[include print/filament.cfg]
[include print/nozzle_guard.cfg] # bd_pressure nozzle collision guard (needs extras/bdp_guard.py)
[include print/homing.cfg]
[include print/idle_timeout.cfg]
[include print/print_start_end.cfg]
[include print/test_speed.cfg] # TEST_SPEED: skipped-steps test
# Heating
[include heating/bed_fans.cfg]
@@ -36,8 +38,10 @@
[include afc/lane_tools.cfg]
[include afc/purge_check.cfg]
[include afc/purge_until_empty.cfg]
[include afc/flex_lanes.cfg] # TPU/TPE lanes: slower long moves
# Pressure advance per lane/spool (bd_pressure + Spoolman)
[include pressure_advance/bd_pressure.cfg] # [bdpressure bd_pa] and the vendor PA_CALIBRATE macros
[include pressure_advance/bd_pa_spoolman.cfg]
# Spoolman
+55 -9
View File
@@ -191,6 +191,10 @@ 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
# Lower test flow (mm3/s) for materials that cannot extrude max_volumetric:
# soft TPU buckles or slips in the extruder at 25. A key contained in the
# lane's material (any case) applies; with several matches the lowest wins.
variable_material_max_volumetric: {"tpu": 8, "tpe": 8, "flex": 8}
variable_acc_wall: 5000
variable_travel_speed: 300
variable_acc_to_decel_factor: 50
@@ -199,12 +203,17 @@ variable_write_threshold: 0.002
# A calibration is rejected (nothing cached or written to Spoolman, the
# previous PA is kept) when:
# - its result is the last PA_CALIBRATE test value: bdpressure.py found no
# result and left the extruder at the end of its scan (0..49 x 0.002),
# result and left the extruder at the end of its scan (0..49 x the PA
# step: 0.098 at the default step, 0.196 for flexibles),
# - the printer paused during the test (e.g. an AFC filament fault),
# - bd_pressure gave fewer than min_points readings or fewer than
# min_distinct different ones (sensor silent or hung; needs
# bdpressure_check.py, otherwise this check is skipped).
variable_sweep_end_pa: 0.098
variable_pa_step: 0.002 # PA_CALIBRATE tests 50 values: 0..49 x step
# Wider range for materials whose PA can be above 0.098 (soft TPU measured
# 0.094): same 50 lines and test time, coarser steps. Key contained in the
# lane's material (any case); with several matches the largest step wins.
variable_material_pa_step: {"tpu": 0.004, "tpe": 0.004, "flex": 0.004}
variable_min_points: 10
variable_min_distinct: 8
# PA applied when a rejected lane has no earlier cached value
@@ -395,8 +404,30 @@ gcode:
# ---------------------------------------------------------------------------
# Core: calibrate (or restore from cache) PA for one lane
# ---------------------------------------------------------------------------
# The lane's temperature (and material) come from Spoolman through AFC,
# but AFC only reads them when a spool is assigned or Klipper starts: a
# nozzle temperature changed in Spoolman afterwards was not used (TPU
# calibrated at 210C after Spoolman was set to 230C). When a calibration
# will run, assign the lane's spool again first so AFC reads Spoolman now.
# The calibration is a separate macro, rendered after that refresh.
[gcode_macro LANE_CALIBRATE_PA]
description: Calibrate PA for one AFC lane using bd_pressure, with caching
description: Calibrate PA for one AFC lane using bd_pressure, with caching (reads the lane's Spoolman data again first)
# LANE=<lane_name> [FORCE=1]
gcode:
{% set lane = params.LANE|default("")|string %}
{% set force = params.FORCE|default(0)|int %}
{% set vars = printer.save_variables.variables %}
{% set spool_id = vars["pa_lane_" ~ lane ~ "_spool_id"]|default(0)|int %}
{% set cached = ("pa_value_spool_" ~ spool_id) in vars %}
{% set key = "AFC_stepper " ~ lane %}
{% set afc_spool = printer[key].spool_id|default(none, true) if key in printer else none %}
{% if lane and spool_id > 0 and (force or not cached) and afc_spool %}
SET_SPOOL_ID LANE={lane} SPOOL_ID={afc_spool}
{% endif %}
_LANE_CALIBRATE_PA_RUN {rawparams}
[gcode_macro _LANE_CALIBRATE_PA_RUN]
description: Helper for LANE_CALIBRATE_PA: calibrate PA for one AFC lane using bd_pressure, with caching
# LANE=<lane_name> [FORCE=1]
gcode:
{% set lane = params.LANE|default("")|string %}
@@ -430,8 +461,22 @@ gcode:
{% set lane_temp = lane_obj.extruder_temp|default(0, true)|float %}
{% set target = printer.extruder.target|default(0)|float %}
{% set nozzle_temp = lane_temp if lane_temp > 0 else (target if target > 0 else cfg.fallback_nozzle_temp|float) %}
{% set material = lane_obj.material|default("", true)|string|lower %}
{% set vol = namespace(max=cfg.max_volumetric|float) %}
{% for key, flow in cfg.material_max_volumetric.items() %}
{% if key in material and flow|float < vol.max %}
{% set vol.max = flow|float %}
{% endif %}
{% endfor %}
{action_respond_info("Calibrating PA for %s (spool %d) at %.0fC..." % (lane, spool_id, nozzle_temp))}
{% set step = namespace(pa=cfg.pa_step|float) %}
{% for key, s in cfg.material_pa_step.items() %}
{% if key in material and s|float > step.pa %}
{% set step.pa = s|float %}
{% endif %}
{% endfor %}
{action_respond_info("Calibrating PA for %s (spool %d) at %.0fC, %g mm3/s, PA 0-%.3f..." % (lane, spool_id, nozzle_temp, vol.max, 49 * step.pa))}
M117 Calibrating PA: {lane}
G1 Z50 F3000
@@ -440,7 +485,7 @@ gcode:
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}
PA_CALIBRATE NOZZLE_TEMP={nozzle_temp} MAX_VOLUMETRIC={vol.max} ACC_WALL={cfg.acc_wall} TRAVEL_SPEED={cfg.travel_speed} ACC_TO_DECEL_FACTOR={cfg.acc_to_decel_factor} PA_STEP={step.pa}
# IMPORTANT: we do NOT read printer.extruder.pressure_advance
# here. Klipper fully evaluates a macro's Jinja2 template
@@ -460,7 +505,7 @@ gcode:
# 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}
_PA_AFTER_CALIBRATE LANE={lane} SPOOL_ID={spool_id} CACHE_FOUND={1 if cache_found else 0} CACHED_PA={cached_pa} SWEEP_END={49 * step.pa}
BLOBIFIER_CLEAN
G4 P400
G1 Z10 F3000
@@ -471,13 +516,14 @@ gcode:
[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>
# LANE=<lane_name> SPOOL_ID=<int> CACHE_FOUND=<0|1> CACHED_PA=<float> SWEEP_END=<last PA tested>
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"] %}
{% set sweep_end = params.SWEEP_END|default(49 * cfg.pa_step|float)|float %}
# This template is evaluated only once this macro is actually
# triggered -- which happens after PA_CALIBRATE (queued earlier by
@@ -486,7 +532,7 @@ gcode:
# Reject runs where the sensor or the printer had a problem
{% set problems = [] %}
{% if new_pa >= cfg.sweep_end_pa|float - 0.0001 %}
{% if new_pa >= sweep_end - 0.0001 %}
{% set _ = problems.append("no result (PA left at the end of the test range, %.3f)" % new_pa) %}
{% endif %}
{% if printer.pause_resume.is_paused %}
@@ -505,7 +551,7 @@ gcode:
{% if problems %}
# A cached value at the end of the scan is an earlier failed run, not a PA
{% set use_cache = cache_found and cached_pa < cfg.sweep_end_pa|float - 0.0001 %}
{% set use_cache = cache_found and cached_pa < sweep_end - 0.0001 %}
{% set keep = cached_pa if use_cache else cfg.fallback_pa|float %}
_WARN MSG="PA calibration for {lane} (spool {spool_id}) rejected: {problems|join('; ')}. Nothing saved; using PA={'%.4f' % keep} ({'cached' if use_cache else 'fallback'}). Fix the cause, then LANE_CALIBRATE_PA LANE={lane} FORCE=1" DISPLAY="PA cal rejected: {lane}, see console"
SET_PRESSURE_ADVANCE ADVANCE={keep}
+167
View File
@@ -0,0 +1,167 @@
# bd_pressure (strain gauge on the nozzle): sensor and the vendor's PA
# calibration macros, from the bd_pressure repo (klipper/bd_pressure.cfg),
# moved here from ~/printer_data/config/bd_pressure.cfg.
#
# Connected over I2C (software I2C on the Nitehawk). Its endstop output is
# not wired; Beacon does the Z probing. Used by: LANE_CALIBRATE_PA
# (bd_pa_spoolman.cfg), the purge check (bdpressure_check.py) and the
# nozzle guard (bdp_guard.py).
#
# PA_CALIBRATE: SET_BDPRESSURE START switches the X and Y drivers off, so
# the X/Y moves of the test only happen on paper (the nozzle stays where
# LANE_CALIBRATE_PA put it) and X/Y are homed again at the end.
# Known: ACC_WALL is read from the ACCEL_TO_DECEL parameter (vendor bug), so
# the test always runs at 4000 mm/s^2; the cached PA values were measured
# like that. The acceleration and square corner velocity it sets are not
# restored (the slicer sets them again per feature).
[bdpressure bd_pa]
port: i2c
i2c_mcu: nhk
i2c_software_scl_pin: nhk:PB3
i2c_software_sda_pin: nhk:PB4
thrhold: 5 # probe trigger value, recommended 3 to 10; bdp_guard and the load check read it as THRHOLD_Z
[gcode_macro PA_RESET]
gcode:
M400
G4 P200
SET_BDPRESSURE NAME=bd_pa COMMAND=RESET_PROBE
G4 P200
[gcode_macro PA_STATE]
gcode:
{% set status = printer["bdpressure bd_pa"].state %}
{ action_respond_info("status:%s %d"%(status,params.T|int)) }
[gcode_macro PA_E]
gcode:
{% set status = printer["bdpressure bd_pa"].state %}
{% set i = params.I|int %}
#{ action_respond_info("status:%f %d"%(params.STEP|float,i)) }
{% if status=='START' %}
{% set pressure_ad = 0 + i*(params.STEP|float) %}
SET_PRESSURE_ADVANCE ADVANCE={pressure_ad}
{% set y_step =38.75 + i*3.5 %}
G1 X78 Y{y_step} F{params.T_SPEED|int}
G1 F{params.L_SPEED|int}
G1 X98 Y{y_step} E.92643
G1 F{params.H_SPEED|int}
G1 X138 Y{y_step} E1.85285
G1 F{params.L_SPEED|int}
G1 X158 Y{y_step} E.92643
M400
SET_BDPRESSURE NAME=bd_pa COMMAND=READ VALUE={pressure_ad}
{% endif %}
[gcode_macro PA_CALIBRATE]
gcode:
# PA_CALIBRATE NOZZLE_TEMP=[first_layer_temperature] MAX_VOLUMETRIC=[filament_max_volumetric_speed] ACC_WALL=[outer_wall_acceleration]
# TRAVEL_SPEED=[travel_speed] ACC_TO_DECEL_FACTOR=[accel_to_decel_factor]
### temperature=210,ACCEL_TO_DECEL=10000,ACCEL=8000,SQUARE_CORNER_VELOCITY=5
{% set NOZZLE_TEMP = 210 %}
{% if params.NOZZLE_TEMP %}
{ action_respond_info("nozzle TEMPERATURE %s "%params.NOZZLE_TEMP) }
{% set NOZZLE_TEMP = params.NOZZLE_TEMP|default(210)|float %}
{% endif %}
{% set ACC_WALL = 4000 %}
{% if params.ACC_WALL %}
{ action_respond_info("ACC_out_WALL %s "%params.ACC_WALL) }
{% set ACC_WALL = params.ACCEL_TO_DECEL|default(4000)|float %}
{% endif %}
{% set SQUARE_CORNER_SPEED = 9 %}
{% if params.SQUARE_CORNER_VELOCITY %}
{ action_respond_info("SQUARE_CORNER_VELOCITY %s "%params.SQUARE_CORNER_VELOCITY) }
{% set SQUARE_CORNER_SPEED = params.SQUARE_CORNER_VELOCITY|default(9)|float %}
{% endif %}
### travel_speed=400, max_volumetric_speed = [filament_max_volumetric_speed]
## low_speed = (1020/20)*max_volumetric_speed
## high_speed = (10740/20)*max_volumetric_speed
{% set ACC_TO_DECEL_FAC = 0.5 %}
{% set MIN_CRUISE = 0.5 %}
{% if params.ACC_TO_DECEL_FACTOR %}
{ action_respond_info("accel_to_decel_factor %s "%params.ACC_TO_DECEL_FACTOR) }
{% set ACC_TO_DECEL_FAC = params.ACC_TO_DECEL_FACTOR.replace("%", "").strip() | float / 100 %}
{% set MIN_CRUISE = 1-ACC_TO_DECEL_FAC|float %}
{ action_respond_info("ACC_TO_DECEL_FACTOR %s "%ACC_TO_DECEL_FAC) }
{% endif %}
{% if params.MINIMUM_CRUISE_RATIO %}
{% set MIN_CRUISE = params.MINIMUM_CRUISE_RATIO|default(0.5)|float %}
{% endif %}
{ action_respond_info("MIN_CRUISE %s "%MIN_CRUISE) }
{% set ACC_TO_DEC = ACC_TO_DECEL_FAC*ACC_WALL %}
{% set TRAVEL_SPEED = 400 %}
{% if params.TRAVEL_SPEED %}
{% set TRAVEL_SPEED = params.TRAVEL_SPEED|default(400)|float * 60 %}
{ action_respond_info("travel_speed %s/60 "%TRAVEL_SPEED) }
{% endif %}
{% set MAX_VOLUMETRIC = 20 %}
{% if params.MAX_VOLUMETRIC %}
{ action_respond_info("filament_max_volumetric_speed %s "%params.MAX_VOLUMETRIC) }
{% set MAX_VOLUMETRIC = params.MAX_VOLUMETRIC|default(20)|float %}
{% endif %}
{% set low_speed = 51*MAX_VOLUMETRIC %}
{ action_respond_info("low_speed %f "%low_speed) }
{% set high_speed = 537*MAX_VOLUMETRIC %}
{ action_respond_info("high_speed %f "%high_speed) }
# volum=[filament_max_volumetric_speed]
# travel=[travel_speed] [accel_to_decel_factor] acc=[outer_wall_acceleration] temper=[first_layer_temperature]
G90
G21
M83
M109 S{NOZZLE_TEMP}
G1 E4
SET_PRESSURE_ADVANCE ADVANCE=0.04; Override pressure advance value
SET_VELOCITY_LIMIT ACCEL={ACC_WALL} ACCEL_TO_DECEL={ACC_TO_DEC}
SET_VELOCITY_LIMIT ACCEL={ACC_WALL} MINIMUM_CRUISE_RATIO={MIN_CRUISE}
SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY={SQUARE_CORNER_SPEED}
SET_STEPPER_ENABLE STEPPER=stepper_x ENABLE=1
SET_STEPPER_ENABLE STEPPER=stepper_y ENABLE=1
SET_BDPRESSURE NAME=bd_pa COMMAND=START
M400
G4 P1000
{% set y_step = 38.75 %}
{% set pressure_ad = 0 %}
# PA_STEP: PA increase per test line, 50 lines (0..49 x step); the
# vendor value 0.002 tests 0-0.098. bdpressure.py reports the VALUE
# sent with each reading, so a larger step widens the range.
{% set pressure_step = params.PA_STEP|default(0.002)|float %}
G1 F{high_speed}
G1 X78 Y{y_step} E9.92204
M400
G4 P4000
G1 F{low_speed}
G1 X98 Y{y_step} E.92643
G1 X138 Y{y_step} E1.85285
G1 X158 Y{y_step} E.92643
G1 X78 Y{y_step} F{TRAVEL_SPEED}
######
M400
{% for i in range(50) %}
PA_E STEP={pressure_step} T_SPEED={TRAVEL_SPEED} H_SPEED={high_speed} L_SPEED={low_speed} I={i}
{% endfor %}
M400
SET_BDPRESSURE NAME=bd_pa COMMAND=STOP
G4 P500
SET_BDPRESSURE NAME=bd_pa COMMAND=RESET_PROBE
G4 P500
SET_KINEMATIC_POSITION X=0 Y=0
G28 XY
# Z was not switched off and not moved: it keeps its position. The
# vendor macro set Z back to z_current, read before any of this ran:
# a Z hop in G28 XY (Beacon home_z_hop) would then leave Klipper
# believing the nozzle is lower than it is.
G4 P200
#G28 Z
+71 -1
View File
@@ -1,8 +1,78 @@
# HOME: home all axes and move to the bed centre
#
# Z always with Beacon contact (the nozzle touches the bed), also when the
# printer is already homed. Without METHOD=CONTACT, [beacon]
# home_method_when_homed = proximity homes a homed printer on the
# eddy-current model of an earlier print (other bed temperature, maybe
# another plate) without touching the bed and without recalibrating.
# Contact refuses a nozzle above 180C (contact_max_hotend_temperature).
[gcode_macro HOME]
gcode:
G90
G4 P2000
G28 X0 Y0 Z0
_GUARD_ON PHASE=home_xy
G28 X Y
# Contact homing presses the nozzle on the bed: guard off
_GUARD_OFF
G28 Z METHOD=CONTACT CALIBRATE=1
_GUARD_ON PHASE=home_lift
G1 X175 Y175 Z10 F1200
_GUARD_OFF
# _Z_CHECK: compare where Klipper thinks the nozzle is with what the Beacon
# measures (proximity), at X/Y (default the bed centre) and Z (default 3 mm,
# inside the model range 0.2-5 mm). Catches a Z that changed since the last
# homing (lost Z steps, gantry moved, much thermal drift) before the nozzle
# goes low; a difference above MAX stops the print. A wrong Beacon model
# fools both readings the same way, so this does not replace contact homing.
#
# BEACON_QUERY stores its reading in printer.beacon.last_sample. When it
# gives no new reading there, the check only warns, so a Beacon version
# without that field does not stop every print.
[gcode_macro _Z_CHECK]
description: Compare Klipper's Z with the Beacon reading at X Y Z (default 175 175 3), stop the print when they differ by more than MAX (default 0.5 mm)
gcode:
{% set x = params.X|default(175)|float %}
{% set y = params.Y|default(175)|float %}
{% set z = params.Z|default(3)|float %}
{% set max = params.MAX|default(0.5)|float %}
{% if 'xyz' not in printer.toolhead.homed_axes %}
_ERROR MSG="_Z_CHECK: printer not homed" DISPLAY="Z check: not homed"
{% elif 'beacon' not in printer %}
_WARN MSG="_Z_CHECK: no [beacon], check skipped"
{% else %}
{% set last = printer.beacon.last_sample %}
{% set prev = last.time if last and last.time is defined else -1 %}
SAVE_GCODE_STATE NAME=_z_check
G90
_GUARD_ON PHASE=z_check
{% if printer.toolhead.position.z < z %}
G1 Z{z} F600
{% endif %}
G1 X{x} Y{y} F12000
G1 Z{z} F600
_GUARD_OFF
G4 P300
BEACON_QUERY
_Z_CHECK_EVAL MAX={max} PREV={prev}
RESTORE_GCODE_STATE NAME=_z_check
{% endif %}
# Separate macro: rendered after BEACON_QUERY has run. toolhead.position is
# the real nozzle height (gcode offset and mesh included).
[gcode_macro _Z_CHECK_EVAL]
description: Helper for _Z_CHECK: compare the BEACON_QUERY reading with the toolhead Z
gcode:
{% set s = printer.beacon.last_sample %}
{% set z = printer.toolhead.position.z %}
{% set max = params.MAX|default(0.5)|float %}
{% if not s or s.time is not defined or s.dist is not defined or s.time == params.PREV|default(-1)|float %}
_WARN MSG="_Z_CHECK: BEACON_QUERY left no new reading in printer.beacon.last_sample, Z check skipped" DISPLAY="Z check skipped"
{% elif s.dist is none %}
_ERROR MSG="_Z_CHECK: Klipper Z={'%.2f' % z} but the Beacon reads nothing (more than 5 mm away or no model), print stopped" DISPLAY="Z check failed, print stopped"
{% elif (s.dist - z)|abs > max %}
_ERROR MSG="_Z_CHECK: Klipper Z={'%.3f' % z}, Beacon measures {'%.3f' % s.dist} mm (difference {'%.3f' % (s.dist - z)}, limit {max}), print stopped" DISPLAY="Z check failed, print stopped"
{% else %}
RESPOND MSG="Z check: Klipper Z={'%.3f' % z}, Beacon {'%.3f' % s.dist} mm, difference {'%.3f' % (s.dist - z)} (limit {max})"
{% endif %}
+35
View File
@@ -0,0 +1,35 @@
# Nozzle collision guard: bd_pressure watches the nozzle force during moves
# that must not touch anything (needs extras/bdp_guard.py and the bd_pressure
# load-check firmware). See extras/bdp_guard.py.
#
# mode: stop shuts Klipper down on a touch; log only measures and reports
# each phase (console and klippy.log, "bdp_guard:"). A phase can override it
# with _GUARD_ON MODE=log (the brush does).
# Measured 2026-10-18: peaks 1-8 in all phases except the brush (47); a
# light finger push on the cold nozzle gives 41.
[bdp_guard]
sensor: bd_pa
mode: stop
threshold: 30
samples: 3
interval: 0.02
# M400 first: BDP_GUARD_ARM/DISARM act at once, not in the move queue, so
# without it the guard would watch moves that are still queued (contact
# homing, extrusion) or stop watching before the armed moves ran.
[gcode_macro _GUARD_ON]
description: Arm the nozzle guard for PHASE=<name> [MODE=log|stop] after the queued moves (nothing without [bdp_guard])
gcode:
{% if 'bdp_guard' in printer %}
M400
BDP_GUARD_ARM PHASE={params.PHASE|default('armed')} {('MODE=' ~ params.MODE) if 'MODE' in params else ''}
{% endif %}
[gcode_macro _GUARD_OFF]
description: Disarm the nozzle guard after the queued moves (nothing without [bdp_guard])
gcode:
{% if 'bdp_guard' in printer %}
M400
BDP_GUARD_DISARM
{% endif %}
+129 -3
View File
@@ -9,16 +9,20 @@
# PRINT_START runs on a soaked bed:
# PRINT_WARMUP bed heats while the nozzle cools and the printer homes,
# wait for the bed, HEAT_SOAK
# PRINT_START QGL, contact Z calibration, mesh, load the initial tool,
# PRINT_START QGL, Z, contact Z calibration, mesh, load the initial tool,
# PA for all lanes, XY reference
# The filament is loaded after the Z calibration, so the contact always
# touches a clean nozzle that has not been oozing at 150C for minutes.
# Chamber prints are the exception: PRINT_WARMUP loads and purges the first
# tool before the chamber heats (_LOAD_FOR_CHAMBER), retracted for the soak;
# BLOBIFIER_CLEAN cleans the nozzle before the contact.
[gcode_macro PRINT_WARMUP]
description: Print start before HEAT_SOAK: home while the bed heats, wait for the bed, soak
gcode:
{% set target_bed = params.BED|int %}
{% set target_chamber = params.CHAMBER|default("0")|int %}
{% set initial_tool = params.TOOL|default("0")|int %}
RESET_FAILURE
CLEAR_PAUSE
@@ -45,10 +49,17 @@ gcode:
# Only to get the toolhead over the bed for the soak; the Z that counts
# is the contact calibration in PRINT_START on the soaked bed
_STATUS STATE=homing
_NOZZLE_CONTACT_TEMP
# HOME homes Z with Beacon contact: wait for a steady nozzle temperature,
# a nozzle still heating spreads the contact samples (6 restarts seen)
_NOZZLE_CONTACT_TEMP WAIT=1
RESPOND MSG="homing"
HOME
{% if target_chamber > 30 %}
_LOAD_FOR_CHAMBER TOOL={initial_tool}
{% endif %}
_GUARD_ON PHASE=warmup_park
G1 X175 Y175 Z5 F1200
_GUARD_OFF
_STATUS STATE=heating_bed
{% if target_bed > 0 %}
@@ -67,6 +78,69 @@ gcode:
HEAT_SOAK HEATER='heater_bed' TARGET={target_bed} SOAKER='temperature_sensor bed_core' SOAK_TEMP={target_bed - 5} TIMEOUT=30 CANCEL='_NOTHING'
{% endif %}
# With the chamber heated, filament in the toolhead path softens: a PLA lane
# still loaded softens in the PTFE tube, and the piece of PLA that a cut
# unload leaves below the cutter softens in the extruder and jams the next
# load (seen: PLA unloaded, ASA jammed after the soak). The piece stays until
# the next load, so an empty toolhead can have it too. For a chamber print,
# load the print's first tool and purge it now, before the chamber heats:
# the purge at the new filament's temperature pushes the leftover out.
# AFC unloads a loaded lane back into the Box Turtle, outside the chamber.
[gcode_macro _LOAD_FOR_CHAMBER]
description: Before a chamber soak: load and purge TOOL (the print's first tool) unless its lane is already loaded
gcode:
{% set tool = 'T' ~ params.TOOL|default(0)|int %}
{% if 'AFC' in printer %}
{% set cur = printer.AFC.current_load %}
{% set ns = namespace(lane=none) %}
{% for l in printer.AFC.lanes if ('AFC_stepper ' ~ l) in printer and printer['AFC_stepper ' ~ l].map == tool %}
{% set ns.lane = l %}
{% endfor %}
{% if ns.lane is none %}
_ERROR MSG="Chamber heating: no AFC lane is mapped to {tool}, print stopped before the chamber heats" DISPLAY="No lane for {tool}, print stopped"
{% elif cur != ns.lane %}
{% set new = printer['AFC_stepper ' ~ ns.lane] %}
# Hot enough to unload the old filament and to purge the new one
{% set temps = [new.extruder_temp|default(0, true)|int, printer.configfile.settings.extruder.min_extrude_temp|int + 20] %}
{% if cur and ('AFC_stepper ' ~ cur) in printer %}
{% set temps = temps + [printer['AFC_stepper ' ~ cur].extruder_temp|default(0, true)|int] %}
RESPOND MSG="Chamber heating: unloading {cur} ({printer['AFC_stepper ' ~ cur].material}), loading and purging {ns.lane} ({new.material}, {tool}) before the chamber heats"
{% else %}
RESPOND MSG="Chamber heating: loading and purging {ns.lane} ({new.material}, {tool}) before the chamber heats"
{% endif %}
# The Blobifier only purges with the QGL applied: without it AFC
# loaded the lane and skipped the purge. Level now (PRINT_START
# levels again on the soaked bed) and home Z after it.
{% if printer.quad_gantry_level and not printer.quad_gantry_level.applied %}
_GUARD_ON PHASE=warmup_qgl
QUAD_GANTRY_LEVEL
_GUARD_ON PHASE=warmup_z_home
G28 Z
_GUARD_OFF
{% endif %}
M109 S{temps|max}
{tool}
_LOAD_FOR_CHAMBER_VERIFY LANE={ns.lane}
{% endif %}
{% endif %}
# Separate macro: rendered after the tool change has run. AFC pauses on a
# failed load instead of raising an error, so check the result here: a
# failed load stops the print before the chamber heats.
[gcode_macro _LOAD_FOR_CHAMBER_VERIFY]
description: Helper for _LOAD_FOR_CHAMBER: stop the print when LANE is not loaded, else retract and cool the nozzle for the soak
gcode:
{% if printer.AFC.current_load != params.LANE %}
_ERROR MSG="Chamber heating: {params.LANE} could not be loaded ({printer.AFC.current_load or 'nothing'} in the toolhead), print stopped before the chamber heats" DISPLAY="Load failed, print stopped"
{% else %}
# New filament, just purged: a retract remembered for the old one is
# gone. Retract so it does not ooze during the soak; PRINT_START
# undoes it over the bucket.
SAVE_VARIABLE VARIABLE=nozzle_retract VALUE=0
_NOZZLE_RETRACT
M104 S150
{% endif %}
# Beacon contact refuses a nozzle above contact_max_hotend_temperature (180C)
[gcode_macro _NOZZLE_CONTACT_TEMP]
description: Nozzle to 150C for Beacon contact; wait only while it is above MAXIMUM (default 170C), WAIT=1 also waits until it is at least 145C
@@ -114,19 +188,38 @@ gcode:
_LIGHTS_ON
M106 S0
# The last homing was before the soak: check that Z still matches the
# Beacon before the gantry goes low. MAX=1.0: the soak itself drifts Z
# a few tenths; the console line shows how much.
_Z_CHECK MAX=1.0
# The bed is soaked now: level and calibrate Z on the final geometry
_STATUS STATE=leveling
RESPOND MSG="quad gantry level"
# Nozzle guard (bd_pressure) on while nothing should touch the nozzle
_GUARD_ON PHASE=qgl
QUAD_GANTRY_LEVEL
# QGL moved the Z steppers and the frame drifted during the soak: home Z
# again (proximity, on the model of the contact homing in PRINT_WARMUP)
# before the brush takes the nozzle below Z0
_GUARD_ON PHASE=z_home
G28 Z
# Own phase, log only: the bristles press on the nozzle (peak 47,
# 4 reads above 30 in a row), stop mode would shut down every start
_GUARD_ON PHASE=brush MODE=log
BLOBIFIER_CLEAN
_GUARD_OFF
_STATUS STATE=calibrating_z
_NOZZLE_CONTACT_TEMP WAIT=1
G28 Z METHOD=CONTACT CALIBRATE=1 ; calibrate z offset and beacon model hot
_STATUS STATE=meshing
RESPOND MSG="meshing"
_GUARD_ON PHASE=mesh
BED_MESH_CALIBRATE RUNS=2 ; bed mesh in scan mode
# From here on the nozzle extrudes and AFC cuts and loads: guard off
_GUARD_OFF
# Heat over the bucket, then undo the retracts of the previous PRINT_END
_STATUS STATE=heating_extruder
@@ -156,6 +249,10 @@ gcode:
# per-reading width compensation made the flow jump between 77 and 119%.
# To use it again: SET_BDWIDTH NAME=fila_width_0 COMMAND=ENABLE_WIDTH
# A large, slow first layer can trip AFC's buffer fault detection
# ("AFC NOT FEEDING"); the slicer switches it back at layer 2
_FIRST_LAYER_FAULT_DETECTION ON=1
RESPOND MSG="starting"
_STATUS STATE=printing
@@ -196,8 +293,9 @@ gcode:
# End-of-print cleanup shared by PRINT_END and CANCEL_PRINT
[gcode_macro _PRINT_CLEANUP]
description: Heaters (incl. chamber heater) and part fan off, flow back to 100%, bdwidth off, mesh and skew cleared
description: Heaters (incl. chamber heater) and part fan off, flow back to 100%, bdwidth off, mesh and skew cleared, nozzle guard off
gcode:
_GUARD_OFF ; a print start that failed while it was armed
TURN_OFF_HEATERS
iHEATER_OFF ; stops the chamber heater control loop too
M107
@@ -210,6 +308,34 @@ gcode:
{% if 'gcode_macro _PURGE_CHECK_CFG' in printer %}
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=ignore_missing_sensor VALUE=False
{% endif %}
_FIRST_LAYER_FAULT_DETECTION ON=0 ; a print that ended during the first layer
# AFC buffer fault detection (filament_error_sensitivity, 0-10, higher =
# pauses sooner) with its own value during the first layer.
# ON=1 at the end of PRINT_START, ON=0 from the slicer's layer change G-code
# at the start of layer 2 (OrcaSlicer, layer_num counts from 0):
# {if layer_num == 1}_FIRST_LAYER_FAULT_DETECTION ON=0{endif}
# _PRINT_CLEANUP also calls ON=0. The normal value is read from the
# [AFC_buffer] config; ON=0 only restores it if ON=1 changed it.
[gcode_macro _FIRST_LAYER_FAULT_DETECTION]
description: AFC filament fault detection with first_layer_sensitivity during the first layer (ON=1), back to the config value (ON=0)
variable_buffer: "Turtle_1"
variable_first_layer_sensitivity: 4 # 0-10: 0 = off, 4 = pause after 70 mm, 8 = after 30 mm
variable_lowered: False
gcode:
{% set key = "afc_buffer " ~ buffer|lower %}
{% set normal = printer.configfile.settings[key].filament_error_sensitivity|default(0)|float if key in printer.configfile.settings else 0 %}
{% if params.ON|default(1)|int %}
{% if normal > 0 and first_layer_sensitivity|float != normal %}
AFC_SET_ERROR_SENSITIVITY BUFFER={buffer} SENSITIVITY={first_layer_sensitivity}
SET_GCODE_VARIABLE MACRO=_FIRST_LAYER_FAULT_DETECTION VARIABLE=lowered VALUE=True
RESPOND MSG="AFC fault detection {first_layer_sensitivity} for the first layer (normally {normal})"
{% endif %}
{% elif lowered %}
AFC_SET_ERROR_SENSITIVITY BUFFER={buffer} SENSITIVITY={normal}
SET_GCODE_VARIABLE MACRO=_FIRST_LAYER_FAULT_DETECTION VARIABLE=lowered VALUE=False
RESPOND MSG="AFC fault detection back to {normal}"
{% endif %}
# Retract/unretract that is remembered across prints and restarts.
# Klipper's G10 only retracts once until a G11, and nothing sends G11 here,
+127
View File
@@ -0,0 +1,127 @@
# TEST_SPEED: skipped-steps test for speed and acceleration (common Voron
# macro, moved here from ~/printer_data/config/Macros/TEST_SPEED.cfg)
[gcode_macro TEST_SPEED]
# Home, get position, throw around toolhead, home again.
# If MCU stepper positions (first line in GET_POSITION) are greater than a full step different (your number of microsteps), then skipping occured.
# We only measure to a full step to accomodate for endstop variance.
# Example: TEST_SPEED SPEED=300 ACCEL=5000 ITERATIONS=10, ZHEIGHT=20
description: Test for max speed and acceleration parameters for the printer. Procedure: Home -> ReadPositionFromMCU -> MovesToolhead@Vel&Accel -> Home -> ReadPositionfromMCU
gcode:
# Speed
{% set speed = params.SPEED|default(printer.configfile.settings.printer.max_velocity)|int %}
# Iterations
{% set iterations = params.ITERATIONS|default(5)|int %}
# Acceleration
{% set accel = params.ACCEL|default(printer.configfile.settings.printer.max_accel)|int %}
# Minimum Cruise Ratio
{% set min_cruise_ratio = params.MIN_CRUISE_RATIO|default(0.5)|float %}
# Bounding inset for large pattern (helps prevent slamming the toolhead into the sides after small skips, and helps to account for machines with imperfectly set dimensions)
{% set bound = params.BOUND|default(20)|int %}
{% set zHeight = params.ZHEIGHT|default(20)|int %}
# Size for small pattern box
{% set smallpatternsize = SMALLPATTERNSIZE|default(20)|int %}
# Large pattern
# Max positions, inset by BOUND
{% set x_min = printer.toolhead.axis_minimum.x + bound %}
{% set x_max = printer.toolhead.axis_maximum.x - bound %}
{% set y_min = printer.toolhead.axis_minimum.y + bound %}
{% set y_max = printer.toolhead.axis_maximum.y - bound %}
# Small pattern at center
# Find X/Y center point
{% set x_center = (printer.toolhead.axis_minimum.x|float + printer.toolhead.axis_maximum.x|float ) / 2 %}
{% set y_center = (printer.toolhead.axis_minimum.y|float + printer.toolhead.axis_maximum.y|float ) / 2 %}
# Set small pattern box around center point
{% set x_center_min = x_center - (smallpatternsize/2) %}
{% set x_center_max = x_center + (smallpatternsize/2) %}
{% set y_center_min = y_center - (smallpatternsize/2) %}
{% set y_center_max = y_center + (smallpatternsize/2) %}
# Save current gcode state (absolute/relative, etc)
SAVE_GCODE_STATE NAME=TEST_SPEED
# Output parameters to g-code terminal
{ action_respond_info("TEST_SPEED: starting %d iterations at speed %d, accel %d" % (iterations, speed, accel)) }
# Home and get position for comparison later:
M400 # Finish moves - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/66
G28
# QGL if not already QGLd (only if QGL section exists in config)
{% if printer.configfile.settings.quad_gantry_level %}
{% if printer.quad_gantry_level.applied == False %}
QUAD_GANTRY_LEVEL
G28 Z
{% endif %}
{% endif %}
# Move 50mm away from max position and home again (to help with hall effect endstop accuracy - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/24)
G90
G1 X{printer.toolhead.axis_maximum.x-50} Y{printer.toolhead.axis_maximum.y-50} F{30*60}
M400 # Finish moves - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/66
G28 X Y
G0 X{printer.toolhead.axis_maximum.x-1} Y{printer.toolhead.axis_maximum.y-1} F{30*60}
G4 P1000
GET_POSITION
# Go to starting position
G0 X{x_min} Y{y_min} Z{zHeight + 10} F{speed*60}
# Set new limits
{% if printer.configfile.settings.printer.minimum_cruise_ratio is defined %}
SET_VELOCITY_LIMIT VELOCITY={speed} ACCEL={accel} MINIMUM_CRUISE_RATIO={min_cruise_ratio}
{% else %}
SET_VELOCITY_LIMIT VELOCITY={speed} ACCEL={accel} ACCEL_TO_DECEL={accel / 2}
{% endif %}
{% for i in range(iterations) %}
# Large pattern diagonals
G0 X{x_min} Y{y_min} F{speed*60}
G0 X{x_max} Y{y_max} F{speed*60}
G0 X{x_min} Y{y_min} F{speed*60}
G0 X{x_max} Y{y_min} F{speed*60}
G0 X{x_min} Y{y_max} F{speed*60}
G0 X{x_max} Y{y_min} F{speed*60}
# Large pattern box
G0 X{x_min} Y{y_min} F{speed*60}
G0 X{x_min} Y{y_max} F{speed*60}
G0 X{x_max} Y{y_max} F{speed*60}
G0 X{x_max} Y{y_min} F{speed*60}
# Small pattern diagonals
G0 X{x_center_min} Y{y_center_min} F{speed*60}
G0 X{x_center_max} Y{y_center_max} F{speed*60}
G0 X{x_center_min} Y{y_center_min} F{speed*60}
G0 X{x_center_max} Y{y_center_min} F{speed*60}
G0 X{x_center_min} Y{y_center_max} F{speed*60}
G0 X{x_center_max} Y{y_center_min} F{speed*60}
# Small pattern box
G0 X{x_center_min} Y{y_center_min} F{speed*60}
G0 X{x_center_min} Y{y_center_max} F{speed*60}
G0 X{x_center_max} Y{y_center_max} F{speed*60}
G0 X{x_center_max} Y{y_center_min} F{speed*60}
{% endfor %}
# Restore max speed/accel/accel_to_decel to their configured values
{% if printer.configfile.settings.printer.minimum_cruise_ratio is defined %}
SET_VELOCITY_LIMIT VELOCITY={printer.configfile.settings.printer.max_velocity} ACCEL={printer.configfile.settings.printer.max_accel} MINIMUM_CRUISE_RATIO={printer.configfile.settings.printer.minimum_cruise_ratio}
{% else %}
SET_VELOCITY_LIMIT VELOCITY={printer.configfile.settings.printer.max_velocity} ACCEL={printer.configfile.settings.printer.max_accel} ACCEL_TO_DECEL={printer.configfile.settings.printer.max_accel_to_decel}
{% endif %}
# Re-home and get position again for comparison:
M400 # Finish moves - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/66
G28 # This is a full G28 to fix an issue with CoreXZ - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/12
# Go to XY home positions (in case your homing override leaves it elsewhere)
G90
G0 X{printer.toolhead.axis_maximum.x-1} Y{printer.toolhead.axis_maximum.y-1} F{30*60}
G4 P1000
GET_POSITION
# Restore previous gcode state (absolute/relative, etc)
RESTORE_GCODE_STATE NAME=TEST_SPEED