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,3 @@
|
||||
# Everything runs on Linux (Klipper host): keep LF line endings,
|
||||
# also when the repo is edited on Windows.
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,150 @@
|
||||
# klipper-macros
|
||||
|
||||
My Klipper macros, the small Klipper modules they need, and helper scripts,
|
||||
from a Voron 2.4 (350) with an AFC Box Turtle (5 lanes), Blobifier, Beacon,
|
||||
Nitehawk-SB toolboard, bd_pressure, bdwidth and an iHeater chamber heater.
|
||||
|
||||
Installs with one script and updates through Moonraker's update manager.
|
||||
|
||||
## Contents
|
||||
|
||||
### Macros (`macros/`, included through `klipper-macros.cfg`)
|
||||
|
||||
| File | Macros | Needs |
|
||||
|---|---|---|
|
||||
| `macros.cfg` | `PRINT_WARMUP`, `PRINT_START`, `PRINT_END`, `SOAK_MY_BED`, `[idle_timeout]` | Beacon, AFC, Blobifier, `HEAT_SOAK`, iHeater, bdwidth, bd_pressure PA macros, soak-my-bed |
|
||||
| `afc_macros.cfg` | `AFC_XY_SAFETY_CHECK`, `_AFC_STORE_HOME_REFERENCE`, `AFC_XY_RESUME`, `LANE_FEED_UNTIL_HUB_OPEN`, `LANE_FEED_ABORT`, `HOME`, `SET_ACTIVE_SPOOL`, `CLEAR_ACTIVE_SPOOL` | AFC, `extras/xy_drift.py` (CoreXY) |
|
||||
| `purge_check.cfg` | `BLOBIFIER_CHECKED`: checks every AFC purge with bd_pressure (and optionally bdwidth motion) | Blobifier, AFC, `extras/bdpressure_check.py` + bd_pressure load-check firmware, `extras/bdwidth_status.py` |
|
||||
| `purge_until_empty.cfg` | `PURGE_UNTIL_EMPTY`: purges a snapped filament out until there is no more nozzle pressure | same as `purge_check.cfg` |
|
||||
| `bd_pa_afc_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 with bd_pressure, cached locally and in Spoolman | bd_pressure, AFC, Spoolman, `gcode_shell_command` |
|
||||
| `motion.cfg` | PIR motion sensor (`[gcode_button am312]`) switching the case light | motion sensor on a pin, `[output_pin caselight]` |
|
||||
| `drying.cfg` | `START_DRYING`, `STOP_DRYING`, `STATUS_DRYING`: dry filament on the heated bed | bed fans macros, chamber heater (optional) |
|
||||
| `bedfans.cfg` | Bed fan control through `SET_HEATER_TEMPERATURE`/`M190`/`M140` overrides | `[fan_generic BedFans]` |
|
||||
| `heatsoak.cfg` | `HEAT_SOAK`, `STOP_HEAT_SOAK`, `CANCEL_HEAT_SOAK`, `HEAT_SOAK_RESUME`, `RESUME` | — |
|
||||
| `iHeater.cfg` | iHeater MCU, heater, sensors, `M141`, `M191`, `iHEATER_ON`, `iHEATER_OFF` | iHeater chamber heater |
|
||||
| `stealthburner_led_effects_barf.cfg` | Stealthburner LED status macros (`_status_*`) | `led_effect`, `[neopixel sb_leds]` |
|
||||
| `macros_klipperscreen.cfg` | `PAUSE`, `RESUME`, `CANCEL_PRINT`, `LOAD_FILAMENT`, `UNLOAD_FILAMENT`, `M600` | — |
|
||||
|
||||
### Klipper modules (`extras/`)
|
||||
|
||||
| File | For |
|
||||
|---|---|
|
||||
| `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 |
|
||||
| `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. |
|
||||
|
||||
### Scripts (`scripts/`)
|
||||
|
||||
Firmware update scripts for all MCUs (Klipper and Katapult). They contain
|
||||
this printer's CAN UUIDs and serial IDs: see [scripts/README.md](scripts/README.md)
|
||||
before using them on another printer.
|
||||
|
||||
## Install
|
||||
|
||||
On the Klipper host:
|
||||
|
||||
```bash
|
||||
cd ~ && git clone https://mangavalk.com/mangavalk/klipper-macros.git
|
||||
~/klipper-macros/install.sh
|
||||
```
|
||||
|
||||
`install.sh`:
|
||||
|
||||
- links `extras/*.py` into `~/klipper/klippy/extras/` (and hides them from
|
||||
Klipper's update manager),
|
||||
- links `macros/` to `~/printer_data/config/klipper-macros`, so the files show
|
||||
up in Mainsail/Fluidd under `klipper-macros/`,
|
||||
- makes the shell scripts executable,
|
||||
- adds this section to `moonraker.conf` (once, with a backup):
|
||||
|
||||
```ini
|
||||
[update_manager klipper-macros]
|
||||
type: git_repo
|
||||
path: ~/klipper-macros
|
||||
origin: https://mangavalk.com/mangavalk/klipper-macros.git
|
||||
primary_branch: main
|
||||
managed_services: klipper
|
||||
install_script: install.sh
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
1. In `printer.cfg`, after `[include mainsail.cfg]`:
|
||||
```ini
|
||||
[include klipper-macros/klipper-macros.cfg]
|
||||
```
|
||||
2. Restart the **Klipper service** (`sudo systemctl restart klipper`, or
|
||||
Mainsail → services → klipper → restart). A `RESTART` or
|
||||
`FIRMWARE_RESTART` does not load new or changed Python modules.
|
||||
3. Restart Moonraker once, so the update manager shows `klipper-macros`.
|
||||
|
||||
Updates then come through Moonraker; it re-runs `install.sh` and restarts
|
||||
Klipper.
|
||||
|
||||
### Switching from copies in the config folder
|
||||
|
||||
If these macros already exist as files in your config folder (as on the
|
||||
original printer: `Macros/*.cfg` and `bd_pa_afc_spoolman.cfg`), remove those
|
||||
includes when adding `klipper-macros.cfg`, otherwise the macros are defined
|
||||
twice and the old copy may win:
|
||||
|
||||
- remove `[include bd_pa_afc_spoolman.cfg]` from `printer.cfg`,
|
||||
- replace `[include ./Macros/macros.cfg]` with
|
||||
`[include klipper-macros/klipper-macros.cfg]`,
|
||||
- keep including what is not in this repo yourself, for example
|
||||
`[include Macros/timelapse.cfg]` and `[include Macros/TEST_SPEED.cfg]`.
|
||||
|
||||
`bd_pa_variables.cfg` (saved PA values) stays where it is, in the config
|
||||
folder; it is data, not part of the repo.
|
||||
|
||||
## Adapting to another printer
|
||||
|
||||
These are the settings of one printer. Before using a file, check:
|
||||
|
||||
- **Names:** `fila_width_0` (bdwidth), `bd_pa` (bd_pressure), `sb_leds`,
|
||||
`caselight`, `BedFans`, `iHeater_H`, lane names `lane1..lane5`.
|
||||
- **Pins and serial IDs:** `motion.cfg` (`PC0`), `iHeater.cfg` (serial ID).
|
||||
- **Positions:** the bed centre `X175 Y175` (350 mm bed) in `HOME`
|
||||
(`afc_macros.cfg`) and in `PRINT_WARMUP`/`PRINT_START` (`macros.cfg`).
|
||||
`AFC_XY_SAFETY_CHECK` uses the axis limits from your config.
|
||||
- **Settings at the top of a file** (`variable_...`): `purge_check.cfg`
|
||||
(`mode: "enforce"`, `motion_check: False` because this printer's bdwidth
|
||||
motion reading is unreliable), `bd_pa_afc_spoolman.cfg` (`spoolman_url`),
|
||||
`bedfans.cfg`, `drying.cfg`.
|
||||
- `xy_drift.py` is for CoreXY only.
|
||||
|
||||
Comment out files you do not need in `klipper-macros.cfg`.
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
~/klipper-macros/install.sh --uninstall
|
||||
```
|
||||
|
||||
Then remove the include from `printer.cfg` and the
|
||||
`[update_manager klipper-macros]` section from `moonraker.conf`, and restart
|
||||
the Klipper service.
|
||||
|
||||
## Credits
|
||||
|
||||
Some files started from other projects and were modified here; their
|
||||
original licenses apply to those parts:
|
||||
|
||||
| File | Based on |
|
||||
|---|---|
|
||||
| `stealthburner_led_effects_barf.cfg` | [tanaes/whopping_Voron_mods](https://github.com/tanaes/whopping_Voron_mods) (Rainbow Barf logo LEDs) |
|
||||
| `bedfans.cfg` | Ellis' bed fan macros ([ellis3dp.com](https://ellis3dp.com/Print-Tuning-Guide/)) |
|
||||
| `heatsoak.cfg` | community `HEAT_SOAK` macro |
|
||||
| `iHeater.cfg` | iHeater vendor configuration |
|
||||
| `drying.cfg` | community filament drying macro |
|
||||
| `macros_klipperscreen.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/) |
|
||||
|
||||
Uses: [Klipper](https://github.com/Klipper3d/klipper),
|
||||
[AFC-Klipper-Add-On](https://github.com/ArmoredTurtle/AFC-Klipper-Add-On),
|
||||
[Blobifier](https://github.com/Dendrowen/Blobifier),
|
||||
[bd_pressure](https://github.com/markniu/bd_pressure),
|
||||
[bdwidth](https://github.com/markniu/bdwidth),
|
||||
[klipper-led_effect](https://github.com/julianschill/klipper-led_effect),
|
||||
[soak-my-bed](https://github.com/marcofailli/soak-my-bed),
|
||||
[Spoolman](https://github.com/Donkie/Spoolman).
|
||||
@@ -0,0 +1,107 @@
|
||||
# Load check using the bd_pressure firmware extension (bdp_check_ext.patch).
|
||||
#
|
||||
# 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
|
||||
# command only reports "not available": nothing is written, because
|
||||
# writing past the stock register table would land in the sensor's RAM.
|
||||
#
|
||||
# Install: copy to ~/klipper/klippy/extras/bdpressure_check.py
|
||||
#
|
||||
# Config:
|
||||
# [bdpressure_check]
|
||||
# sensor: bd_pa # name after "bdpressure" in [bdpressure bd_pa]
|
||||
#
|
||||
# Commands (run them after M400, they act immediately, not in the move queue):
|
||||
# BDP_CHECK_START freeze the baseline and start measuring
|
||||
# BDP_CHECK_STOP stop measuring and read the result
|
||||
# BDP_CHECK_QUERY read the current values without stopping
|
||||
#
|
||||
# Status (printer.bdpressure_check), updated by the commands above:
|
||||
# available, running, peak, above_ms, longest_ms, delta
|
||||
|
||||
REG_EXT = 54 # first byte of the extension
|
||||
REG_CTRL = 56
|
||||
EXT_LEN = 12 # bytes 54-65
|
||||
EXT_MAGIC = (0xBD, 0x01)
|
||||
|
||||
|
||||
class BDPressureCheck:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.sensor_name = 'bdpressure ' + config.get('sensor', 'bd_pa')
|
||||
self.i2c = None
|
||||
self.status = {'available': False, 'running': False, 'peak': 0,
|
||||
'above_ms': 0, 'longest_ms': 0, 'delta': 0}
|
||||
self.printer.register_event_handler('klippy:connect',
|
||||
self._handle_connect)
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command('BDP_CHECK_START', self.cmd_START,
|
||||
desc="Start a bd_pressure load check")
|
||||
gcode.register_command('BDP_CHECK_STOP', self.cmd_STOP,
|
||||
desc="Stop the bd_pressure load check and read it")
|
||||
gcode.register_command('BDP_CHECK_QUERY', self.cmd_QUERY,
|
||||
desc="Read the bd_pressure load check values")
|
||||
|
||||
def _handle_connect(self):
|
||||
sensor = self.printer.lookup_object(self.sensor_name)
|
||||
self.i2c = getattr(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:
|
||||
self.status['available'] = False
|
||||
return False
|
||||
u16 = lambda i: data[i] | (data[i + 1] << 8)
|
||||
delta = u16(10)
|
||||
self.status.update({
|
||||
'available': True,
|
||||
'running': bool(data[3] & 1),
|
||||
'peak': u16(4),
|
||||
'above_ms': u16(6),
|
||||
'longest_ms': u16(8),
|
||||
'delta': delta - 0x10000 if delta & 0x8000 else delta,
|
||||
})
|
||||
return True
|
||||
|
||||
def _respond(self, gcmd, prefix):
|
||||
s = self.status
|
||||
if not s['available']:
|
||||
gcmd.respond_info("%s: bd_pressure check extension not found "
|
||||
"(stock firmware?), nothing done" % prefix)
|
||||
return
|
||||
gcmd.respond_info("%s: running=%d peak=%d above=%d ms longest=%d ms "
|
||||
"delta=%d" % (prefix, s['running'], s['peak'],
|
||||
s['above_ms'], s['longest_ms'],
|
||||
s['delta']))
|
||||
|
||||
def cmd_START(self, gcmd):
|
||||
# The values read here are still the previous check's, so only
|
||||
# confirm the start instead of printing them.
|
||||
if self._read():
|
||||
self.i2c.i2c_write([REG_CTRL, 1])
|
||||
self.status.update({'running': True, 'peak': 0, 'above_ms': 0,
|
||||
'longest_ms': 0})
|
||||
gcmd.respond_info("BDP_CHECK_START: started")
|
||||
return
|
||||
self._respond(gcmd, "BDP_CHECK_START")
|
||||
|
||||
def cmd_STOP(self, gcmd):
|
||||
if self._read():
|
||||
self.i2c.i2c_write([REG_CTRL, 0])
|
||||
self._read()
|
||||
self._respond(gcmd, "BDP_CHECK_STOP")
|
||||
|
||||
def cmd_QUERY(self, gcmd):
|
||||
self._read()
|
||||
self._respond(gcmd, "BDP_CHECK_QUERY")
|
||||
|
||||
def get_status(self, eventtime):
|
||||
return dict(self.status)
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return BDPressureCheck(config)
|
||||
@@ -0,0 +1,71 @@
|
||||
# Expose bdwidth's motion counter and width reading to gcode macros.
|
||||
#
|
||||
# bdwidth.py replaces its own get_status() with the runout helper's, so
|
||||
# macros only see filament_detected/enabled. This read-only wrapper looks
|
||||
# up the existing [bdwidth <name>] object and reports the values it
|
||||
# already keeps. It never talks to the sensor, so bdwidth.py stays
|
||||
# untouched and updates to bdwidth do not overwrite this file.
|
||||
#
|
||||
# Install: copy to ~/klipper/klippy/extras/bdwidth_status.py
|
||||
#
|
||||
# Config:
|
||||
# [bdwidth_status]
|
||||
# sensor: fila_width_0 # name after "bdwidth" in [bdwidth fila_width_0]
|
||||
#
|
||||
# Status (printer.bdwidth_status):
|
||||
# total_move - motion counter since Klipper start (raw counts, signed)
|
||||
# last_motion - counts seen in the last sample
|
||||
# diameter - last width reading in mm (0.0 on a bad reading)
|
||||
# name - the sensor name, for SET_BDWIDTH NAME=...
|
||||
# reading - True while bdwidth is polling the sensor (after
|
||||
# SET_BDWIDTH ... ENABLE..., until DISABLE)
|
||||
# enabled - runout response on (SET_FILAMENT_SENSOR ENABLE=1/0);
|
||||
# says nothing about whether bdwidth is reading
|
||||
# active - "motion", "width", "all" or "disable" (SET_BDWIDTH). After
|
||||
# a Klipper restart it shows the config value, but bdwidth
|
||||
# only starts reading on the first SET_BDWIDTH ... ENABLE
|
||||
# linear_motion - counts per mm (motion_linear_coefficient)
|
||||
# sample_time - seconds between sensor reads
|
||||
|
||||
|
||||
class BDWidthStatus:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.name = config.get('sensor', 'fila_width_0')
|
||||
self.sensor_name = 'bdwidth ' + self.name
|
||||
self.sensor = None
|
||||
self.printer.register_event_handler('klippy:connect',
|
||||
self._handle_connect)
|
||||
|
||||
def _handle_connect(self):
|
||||
self.sensor = self.printer.lookup_object(self.sensor_name)
|
||||
|
||||
def get_status(self, eventtime):
|
||||
s = self.sensor
|
||||
# bdwidth's own get_status() is the runout helper's, which carries
|
||||
# the SET_FILAMENT_SENSOR enable flag
|
||||
try:
|
||||
enabled = bool(s.get_status(eventtime).get('enabled', True))
|
||||
except Exception:
|
||||
enabled = True
|
||||
# bdwidth reads from a reactor timer that only SET_BDWIDTH ... ENABLE
|
||||
# starts and DISABLE stops; is_active alone does not show it
|
||||
timer = getattr(s, 'extrude_factor_update_timer', None)
|
||||
never = self.printer.get_reactor().NEVER
|
||||
reading = (timer is not None and getattr(timer, 'waketime', never) != never
|
||||
and 'disable' not in str(getattr(s, 'is_active', 'disable')))
|
||||
return {
|
||||
'name': self.name,
|
||||
'reading': reading,
|
||||
'enabled': enabled,
|
||||
'total_move': getattr(s, 'actual_total_move', 0),
|
||||
'last_motion': getattr(s, 'lastMotionReading', 0),
|
||||
'diameter': round(getattr(s, 'lastFilamentWidthReading', 0.), 3),
|
||||
'active': str(getattr(s, 'is_active', 'disable')),
|
||||
'linear_motion': getattr(s, 'linear_motion', 42.8),
|
||||
'sample_time': getattr(s, 'sample_time', 1.),
|
||||
}
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return BDWidthStatus(config)
|
||||
@@ -0,0 +1,104 @@
|
||||
# Measure lost XY steps on a CoreXY printer by re-homing.
|
||||
#
|
||||
# Homing resets Klipper's idea of the position, so the toolhead position
|
||||
# after G28 is always the endstop position and cannot show lost steps. The
|
||||
# stepper drivers' step counters (MCU positions) are not reset by homing:
|
||||
# when no steps were lost, the endstop triggers at the same step count as
|
||||
# last time; every lost step shows up as a difference. This module records
|
||||
# the step counts at each X and Y endstop trigger and reports the drift in
|
||||
# mm against a stored reference.
|
||||
#
|
||||
# Install: copy to ~/klipper/klippy/extras/xy_drift.py and add [xy_drift]
|
||||
#
|
||||
# Commands:
|
||||
# XY_DRIFT_STORE use the last X and Y homing as the reference
|
||||
# XY_DRIFT_QUERY print the drift of the last homing against the reference
|
||||
#
|
||||
# Status (printer.xy_drift):
|
||||
# ref_set a reference is stored (cleared by a Klipper restart,
|
||||
# which also resets the step counters)
|
||||
# homed_x, homed_y the axis was homed since the reference was stored
|
||||
# x, y drift in mm of the last homing against the reference
|
||||
# (positive = Klipper had moved further than the head did)
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
class XYDrift:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.sx = self.sy = None
|
||||
self.last = {} # axis -> kinematic position (mm) at the trigger
|
||||
self.ref = {}
|
||||
self.since_ref = set()
|
||||
self.printer.register_event_handler('klippy:connect',
|
||||
self._handle_connect)
|
||||
self.printer.register_event_handler('homing:home_rails_end',
|
||||
self._handle_home_rails_end)
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command('XY_DRIFT_STORE', self.cmd_STORE,
|
||||
desc="Store the last XY homing as drift reference")
|
||||
gcode.register_command('XY_DRIFT_QUERY', self.cmd_QUERY,
|
||||
desc="Report XY drift against the reference")
|
||||
|
||||
def _handle_connect(self):
|
||||
kin = self.printer.lookup_object('toolhead').get_kinematics()
|
||||
steppers = {s.get_name(): s for s in kin.get_steppers()}
|
||||
self.sx = steppers.get('stepper_x')
|
||||
self.sy = steppers.get('stepper_y')
|
||||
if self.sx is None or self.sy is None:
|
||||
raise self.printer.config_error(
|
||||
"xy_drift needs stepper_x and stepper_y (CoreXY)")
|
||||
|
||||
def _handle_home_rails_end(self, homing_state, rails):
|
||||
# This event also comes from other modules (Beacon sends it with no
|
||||
# rails and a state that raises on get_trigger_position), so only
|
||||
# handle X/Y homing and never let an error escape: an exception
|
||||
# here shuts Klipper down.
|
||||
try:
|
||||
names = [rail.get_name() for rail in rails]
|
||||
axes = [axis for axis, st in (('x', self.sx), ('y', self.sy))
|
||||
if st is not None and st.get_name() in names]
|
||||
if not axes:
|
||||
return
|
||||
a = (homing_state.get_trigger_position(self.sx.get_name())
|
||||
* self.sx.get_step_dist())
|
||||
b = (homing_state.get_trigger_position(self.sy.get_name())
|
||||
* self.sy.get_step_dist())
|
||||
except Exception:
|
||||
logging.exception("xy_drift: could not read trigger positions")
|
||||
return
|
||||
# CoreXY: x = (a + b) / 2, y = (a - b) / 2
|
||||
pos = {'x': 0.5 * (a + b), 'y': 0.5 * (a - b)}
|
||||
for axis in axes:
|
||||
self.last[axis] = pos[axis]
|
||||
self.since_ref.add(axis)
|
||||
|
||||
def _drift(self, axis):
|
||||
if axis in self.ref and axis in self.last:
|
||||
return round(self.last[axis] - self.ref[axis], 4)
|
||||
return 0.
|
||||
|
||||
def cmd_STORE(self, gcmd):
|
||||
if 'x' not in self.last or 'y' not in self.last:
|
||||
raise gcmd.error("XY_DRIFT_STORE: home X and Y first")
|
||||
self.ref = dict(self.last)
|
||||
self.since_ref = set()
|
||||
gcmd.respond_info("XY_DRIFT: reference stored")
|
||||
|
||||
def cmd_QUERY(self, gcmd):
|
||||
if not self.ref:
|
||||
gcmd.respond_info("XY_DRIFT: no reference stored")
|
||||
return
|
||||
gcmd.respond_info("XY_DRIFT: X=%.3f mm Y=%.3f mm" % (
|
||||
self._drift('x'), self._drift('y')))
|
||||
|
||||
def get_status(self, eventtime):
|
||||
return {'ref_set': bool(self.ref),
|
||||
'homed_x': 'x' in self.since_ref,
|
||||
'homed_y': 'y' in self.since_ref,
|
||||
'x': self._drift('x'), 'y': self._drift('y')}
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return XYDrift(config)
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install klipper-macros on a Klipper host (Raspberry Pi etc.)
|
||||
#
|
||||
# cd ~ && git clone https://mangavalk.com/mangavalk/klipper-macros.git
|
||||
# ~/klipper-macros/install.sh
|
||||
#
|
||||
# What it does (safe to run again, Moonraker runs it after every update):
|
||||
# 1. links extras/*.py into ~/klipper/klippy/extras/
|
||||
# 2. links macros/ to ~/printer_data/config/klipper-macros
|
||||
# 3. makes the shell scripts executable
|
||||
# 4. adds an [update_manager klipper-macros] section to moonraker.conf
|
||||
# (once, with a backup of moonraker.conf)
|
||||
#
|
||||
# It does not edit printer.cfg; see README.md for the include line.
|
||||
#
|
||||
# ~/klipper-macros/install.sh --uninstall remove the links again
|
||||
set -euo pipefail
|
||||
|
||||
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
KLIPPER_DIR="${KLIPPER_DIR:-$HOME/klipper}"
|
||||
CONFIG_DIR="${CONFIG_DIR:-$HOME/printer_data/config}"
|
||||
MOONRAKER_CONF="$CONFIG_DIR/moonraker.conf"
|
||||
LINK_NAME="$CONFIG_DIR/klipper-macros"
|
||||
EXTRAS_DIR="$KLIPPER_DIR/klippy/extras"
|
||||
ORIGIN="$(git -C "$REPO_DIR" remote get-url origin 2>/dev/null || echo "https://mangavalk.com/mangavalk/klipper-macros.git")"
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
[[ -d $EXTRAS_DIR ]] || die "Klipper not found at $KLIPPER_DIR (set KLIPPER_DIR=...)"
|
||||
[[ -d $CONFIG_DIR ]] || die "config folder not found at $CONFIG_DIR (set CONFIG_DIR=...)"
|
||||
|
||||
if [[ ${1:-} == --uninstall ]]; then
|
||||
for py in "$REPO_DIR"/extras/*.py; do
|
||||
target="$EXTRAS_DIR/$(basename "$py")"
|
||||
if [[ -L $target && $(readlink -f "$target") == "$(readlink -f "$py")" ]]; then
|
||||
rm "$target" && echo "removed $target"
|
||||
fi
|
||||
done
|
||||
[[ -L $LINK_NAME ]] && rm "$LINK_NAME" && echo "removed $LINK_NAME"
|
||||
echo "Remove [include klipper-macros/...] from printer.cfg and the"
|
||||
echo "[update_manager klipper-macros] section from moonraker.conf yourself,"
|
||||
echo "then restart the Klipper service."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 1. Klipper modules
|
||||
for py in "$REPO_DIR"/extras/*.py; do
|
||||
target="$EXTRAS_DIR/$(basename "$py")"
|
||||
if [[ -e $target && ! -L $target ]]; then
|
||||
mv "$target" "$target.bak" && echo "existing $target moved to $target.bak"
|
||||
fi
|
||||
ln -sfn "$py" "$target"
|
||||
echo "linked $target"
|
||||
done
|
||||
# Klipper's update manager would report the links as untracked files
|
||||
exclude="$KLIPPER_DIR/.git/info/exclude"
|
||||
if [[ -f $exclude ]]; then
|
||||
for py in "$REPO_DIR"/extras/*.py; do
|
||||
line="klippy/extras/$(basename "$py")"
|
||||
grep -qxF "$line" "$exclude" || echo "$line" >> "$exclude"
|
||||
done
|
||||
fi
|
||||
|
||||
# 2. Macros
|
||||
if [[ -e $LINK_NAME && ! -L $LINK_NAME ]]; then
|
||||
die "$LINK_NAME exists and is not a link; move it away first"
|
||||
fi
|
||||
ln -sfn "$REPO_DIR/macros" "$LINK_NAME"
|
||||
echo "linked $LINK_NAME"
|
||||
|
||||
# 3. Scripts
|
||||
chmod +x "$REPO_DIR"/macros/*.sh "$REPO_DIR"/scripts/*.sh "$REPO_DIR"/install.sh
|
||||
|
||||
# 4. Moonraker update manager
|
||||
if [[ -f $MOONRAKER_CONF ]] && ! grep -q '^\[update_manager klipper-macros\]' "$MOONRAKER_CONF"; then
|
||||
cp "$MOONRAKER_CONF" "$MOONRAKER_CONF.bak.$(date +%Y%m%d_%H%M%S)"
|
||||
cat >> "$MOONRAKER_CONF" <<EOF
|
||||
|
||||
[update_manager klipper-macros]
|
||||
type: git_repo
|
||||
path: $REPO_DIR
|
||||
origin: $ORIGIN
|
||||
primary_branch: main
|
||||
managed_services: klipper
|
||||
install_script: install.sh
|
||||
EOF
|
||||
echo "added [update_manager klipper-macros] to moonraker.conf"
|
||||
echo "restart Moonraker to see it in the update manager"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done. Next steps (see README.md):
|
||||
1. add to printer.cfg: [include klipper-macros/klipper-macros.cfg]
|
||||
and remove the old includes of the same macro files
|
||||
2. restart the Klipper SERVICE (not just RESTART), so the new Python
|
||||
modules are loaded: sudo systemctl restart klipper
|
||||
EOF
|
||||
@@ -0,0 +1,193 @@
|
||||
#####################################################################
|
||||
## XY lost-step check (needs xy_drift.py + [xy_drift])
|
||||
#####################################################################
|
||||
# Homing resets the toolhead position, so the position after G28 cannot
|
||||
# show lost steps. [xy_drift] compares the stepper step counts at the X and
|
||||
# Y endstop triggers instead: every lost step shows up as drift.
|
||||
# _AFC_STORE_HOME_REFERENCE (PRINT_START) stores the reference, every
|
||||
# AFC_XY_SAFETY_CHECK compares against it and then stores a new reference,
|
||||
# so each check reports the drift since the previous one.
|
||||
|
||||
[xy_drift]
|
||||
|
||||
[gcode_macro _AFC_STORE_HOME_REFERENCE]
|
||||
gcode:
|
||||
G28 X Y
|
||||
M400
|
||||
XY_DRIFT_STORE
|
||||
|
||||
[gcode_macro AFC_XY_SAFETY_CHECK]
|
||||
description: Re-home XY and report steps lost since the last check
|
||||
variable_allowed_mm: 0.1 # one motor skipping one cycle (0.4 mm) shows as 0.2 mm in X and Y; endstop noise ~0.02
|
||||
variable_soft_fail: 1 # 1 = PAUSE on drift, 0 = M112
|
||||
gcode:
|
||||
{% set th = printer.toolhead %}
|
||||
M117 AFC XY check...
|
||||
SAVE_GCODE_STATE NAME=_AFC_XY_CHECK
|
||||
G90
|
||||
G1 Z{[th.position.z + 10, th.axis_maximum.z]|min} F3000
|
||||
G28 X Y
|
||||
M400
|
||||
_AFC_CHECK_CAPTURE
|
||||
RESTORE_GCODE_STATE NAME=_AFC_XY_CHECK
|
||||
|
||||
[gcode_macro _AFC_CHECK_CAPTURE]
|
||||
gcode:
|
||||
{% set d = printer.xy_drift %}
|
||||
{% set cfg = printer["gcode_macro AFC_XY_SAFETY_CHECK"] %}
|
||||
{% set th = printer.toolhead %}
|
||||
{% set lim = cfg.allowed_mm|float %}
|
||||
{% set drift = d.ref_set and (d.x|abs > lim or d.y|abs > lim) %}
|
||||
{% if not d.ref_set %}
|
||||
RESPOND PREFIX="AFC" MSG="XY check: no reference yet (Klipper restarted?), storing one now"
|
||||
{% else %}
|
||||
RESPOND PREFIX="AFC" MSG="XY drift since last check: X={'%.3f' % d.x} Y={'%.3f' % d.y} mm (limit {lim})"
|
||||
{% endif %}
|
||||
# New reference from this homing, so the next check reports only new drift
|
||||
XY_DRIFT_STORE
|
||||
{% if not drift %}
|
||||
{% if d.ref_set %}
|
||||
RESPOND PREFIX="AFC" MSG="Position OK"
|
||||
{% endif %}
|
||||
G1 X{th.axis_maximum.x - 10} Y{th.axis_maximum.y - 10} F5000
|
||||
{% elif cfg.soft_fail|int == 1 %}
|
||||
RESPOND TYPE=error MSG="AFC: XY drift detected! X={'%.3f' % d.x} Y={'%.3f' % d.y} mm. Axes are re-homed, print PAUSED. Run AFC_XY_RESUME to continue, or CANCEL_PRINT."
|
||||
M117 AFC: XY drift! See console. Paused.
|
||||
PAUSE
|
||||
{% else %}
|
||||
RESPOND TYPE=error MSG="AFC: XY drift X={'%.3f' % d.x} Y={'%.3f' % d.y} mm -> M112"
|
||||
M112
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro AFC_XY_RESUME]
|
||||
description: Acknowledge XY drift warning and resume print
|
||||
gcode:
|
||||
RESPOND PREFIX="AFC" MSG="User acknowledged drift, resuming."
|
||||
# Axes were re-homed by the check, so AFC's resume returns to the
|
||||
# correct print position.
|
||||
RESUME
|
||||
|
||||
#####################################################################
|
||||
## unloading help
|
||||
#####################################################################
|
||||
|
||||
[gcode_macro LANE_FEED_UNTIL_HUB_OPEN]
|
||||
variable_lane: 1
|
||||
variable_step: -50
|
||||
variable_max_dist: 2000
|
||||
variable_moved: 0
|
||||
variable_active: 0
|
||||
|
||||
gcode:
|
||||
{% set lane = params.LANE|default(1)|int %}
|
||||
{% set step = params.STEP|default(-50)|float %}
|
||||
{% set max_dist = params.MAX_DIST|default(2000)|float %}
|
||||
|
||||
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=lane VALUE={lane}
|
||||
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=step VALUE={step}
|
||||
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=max_dist VALUE={max_dist}
|
||||
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=moved VALUE=0
|
||||
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=1
|
||||
|
||||
RESPOND PREFIX="info" MSG="START lane={lane} step={step} max={max_dist}"
|
||||
|
||||
UPDATE_DELAYED_GCODE ID=_lane_feed_loop DURATION=0.05
|
||||
|
||||
[delayed_gcode _lane_feed_loop]
|
||||
gcode:
|
||||
{% set m = printer["gcode_macro LANE_FEED_UNTIL_HUB_OPEN"] %}
|
||||
|
||||
{% if m.active == 1 %}
|
||||
|
||||
LANE_MOVE LANE=lane{m.lane} DISTANCE={m.step}
|
||||
|
||||
{% set new_moved = m.moved + m.step %}
|
||||
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=moved VALUE={new_moved}
|
||||
|
||||
RESPOND PREFIX="info" MSG="MOVE lane={m.lane} step={m.step} moved={new_moved}"
|
||||
|
||||
QUERY_ENDSTOPS
|
||||
|
||||
UPDATE_DELAYED_GCODE ID=_lane_eval DURATION=0.03
|
||||
|
||||
{% else %}
|
||||
RESPOND PREFIX="info" MSG="feed_loop inactive"
|
||||
{% endif %}
|
||||
|
||||
[delayed_gcode _lane_eval]
|
||||
gcode:
|
||||
{% set m = printer["gcode_macro LANE_FEED_UNTIL_HUB_OPEN"] %}
|
||||
|
||||
{% if m.active == 1 %}
|
||||
|
||||
{% set hub = printer.query_endstops.last_query.get("hub", "unknown") %}
|
||||
|
||||
# RESPOND PREFIX="info" MSG="QUERY hub={hub} raw={printer.query_endstops.last_query.get('hub', 'MISSING')}"
|
||||
# RESPOND PREFIX="info" MSG="last_query: {printer.query_endstops.last_query}"
|
||||
|
||||
{% if hub == "open" or hub == 0 %}
|
||||
RESPOND PREFIX="info" MSG="HUB TRIGGERED → STOP"
|
||||
|
||||
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=0
|
||||
|
||||
LANE_MOVE LANE=lane{m.lane} DISTANCE=-100
|
||||
|
||||
{% elif m.moved|abs >= m.max_dist %}
|
||||
|
||||
RESPOND PREFIX="warning" MSG="MAX DIST STOP reached={m.moved}"
|
||||
|
||||
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=0
|
||||
|
||||
{% else %}
|
||||
|
||||
RESPOND PREFIX="info" MSG="CONTINUE"
|
||||
|
||||
UPDATE_DELAYED_GCODE ID=_lane_feed_loop DURATION=0.03
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
RESPOND PREFIX="info" MSG="eval inactive"
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro LANE_FEED_ABORT]
|
||||
gcode:
|
||||
SET_GCODE_VARIABLE MACRO=LANE_FEED_UNTIL_HUB_OPEN VARIABLE=active VALUE=0
|
||||
UPDATE_DELAYED_GCODE ID=_lane_feed_loop DURATION=0
|
||||
UPDATE_DELAYED_GCODE ID=_lane_eval DURATION=0
|
||||
RESPOND PREFIX="warning" MSG="ABORTED"
|
||||
|
||||
[gcode_macro HOME]
|
||||
gcode:
|
||||
G90
|
||||
G4 P2000
|
||||
G28 X0 Y0 Z0
|
||||
G1 X175 Y175 Z10 F1200
|
||||
|
||||
[gcode_macro SET_ACTIVE_SPOOL]
|
||||
gcode:
|
||||
{% if params.ID %}
|
||||
{% set id = params.ID|int %}
|
||||
{action_call_remote_method(
|
||||
"spoolman_set_active_spool",
|
||||
spool_id=id
|
||||
)}
|
||||
{% else %}
|
||||
{action_respond_info("Parameter 'ID' is required")}
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro CLEAR_ACTIVE_SPOOL]
|
||||
gcode:
|
||||
{action_call_remote_method(
|
||||
"spoolman_set_active_spool",
|
||||
spool_id=None
|
||||
)}
|
||||
|
||||
# [gcode_macro TEST_AMS_STRESS]
|
||||
# gcode:
|
||||
# {% for i in range(500) %}
|
||||
# T0 ; Load tool 0
|
||||
# G4 P5000 ; Wait 5s
|
||||
# T1 ; Unload
|
||||
# G4 P3000 ; Wait 3s
|
||||
# {% endfor %}
|
||||
@@ -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)}
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
BASE_URL=$1
|
||||
FIELD=$2
|
||||
curl -sf -X POST "${BASE_URL}/api/v1/field/spool/${FIELD}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"name\": \"Pressure Advance\", \"field_type\": \"float\", \"default_value\": \"0\"}" \
|
||||
> /tmp/pa_setup_result 2>&1
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
SPOOL_ID=$1
|
||||
BASE_URL=$2
|
||||
FIELD=$3
|
||||
PA_VALUE=$4
|
||||
CURRENT=$(curl -sf "${BASE_URL}/api/v1/spool/${SPOOL_ID}" 2>/dev/null)
|
||||
EXTRA=$(echo "$CURRENT" | python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
d = json.load(sys.stdin)
|
||||
e = d.get('extra', {})
|
||||
e['$FIELD'] = '$PA_VALUE'
|
||||
print(json.dumps(e))
|
||||
except Exception:
|
||||
print(json.dumps({'$FIELD': '$PA_VALUE'}))
|
||||
" 2>/dev/null)
|
||||
if [ -z "$EXTRA" ]; then
|
||||
EXTRA="{\"$FIELD\": \"$PA_VALUE\"}"
|
||||
fi
|
||||
curl -sf -X PATCH "${BASE_URL}/api/v1/spool/${SPOOL_ID}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"extra\": $EXTRA}" > /tmp/pa_spoolman_write_result 2>&1
|
||||
@@ -0,0 +1,171 @@
|
||||
############### Config options ##################
|
||||
|
||||
[gcode_macro _BEDFANVARS]
|
||||
variable_threshold: 50 # If bed temp target is above this threshold, fans will be enabled. If temp is set to below this threshold, fans will be disabled.
|
||||
variable_fast: 1.0 # Fan speed once bed temp is reached
|
||||
variable_slow: 0.5 # Fan speed while bed is heating
|
||||
variable_cooldown_temp: 40 # NEW: fans stop below this temp
|
||||
gcode:
|
||||
|
||||
########## Aliases #########
|
||||
|
||||
[gcode_macro BEDFANSSLOW]
|
||||
gcode:
|
||||
# Vars
|
||||
{% set SLOW = printer["gcode_macro _BEDFANVARS"].slow|float %}
|
||||
|
||||
SET_FAN_SPEED FAN=BedFans SPEED={SLOW}
|
||||
|
||||
[gcode_macro BEDFANSFAST]
|
||||
gcode:
|
||||
# Vars
|
||||
{% set FAST = printer["gcode_macro _BEDFANVARS"].fast|float %}
|
||||
|
||||
SET_FAN_SPEED FAN=BedFans SPEED={FAST}
|
||||
|
||||
[gcode_macro BEDFANSOFF]
|
||||
gcode:
|
||||
SET_FAN_SPEED FAN=BedFans SPEED=0
|
||||
|
||||
############ Command overrides ############
|
||||
|
||||
# Override, set fan speeds to low and start monitoring loop.
|
||||
[gcode_macro SET_HEATER_TEMPERATURE]
|
||||
rename_existing: _SET_HEATER_TEMPERATURE
|
||||
gcode:
|
||||
# Parameters
|
||||
{% set HEATER = params.HEATER|default("None") %}
|
||||
{% set TARGET = params.TARGET|default(0)|int %}
|
||||
# Vars
|
||||
{% set THRESHOLD = printer["gcode_macro _BEDFANVARS"].threshold|int %}
|
||||
{% set COOLDOWN = printer["gcode_macro _BEDFANVARS"].cooldown_temp|int %}
|
||||
|
||||
{% if HEATER|lower == "extruder" %}
|
||||
M104 S{TARGET}
|
||||
|
||||
# {% elif HEATER|lower == "iHeater_H" %}
|
||||
# _SET_HEATER_TEMPERATURE {rawparams}
|
||||
|
||||
{% elif HEATER|lower == "heater_bed" %}
|
||||
M99140 S{TARGET}
|
||||
|
||||
# Always cancel cooldown loop first (prevents overlap)
|
||||
UPDATE_DELAYED_GCODE ID=bedfancooldown DURATION=0
|
||||
|
||||
# --- NEW LOGIC BLOCK ---
|
||||
{% if TARGET == 0 %}
|
||||
# Bed turning off → start cooldown behavior
|
||||
{% if printer.heater_bed.temperature|int > COOLDOWN %}
|
||||
BEDFANSSLOW
|
||||
UPDATE_DELAYED_GCODE ID=bedfancooldown DURATION=5
|
||||
{% else %}
|
||||
BEDFANSOFF
|
||||
{% endif %}
|
||||
|
||||
# Stop normal heating loop if running
|
||||
UPDATE_DELAYED_GCODE ID=bedfanloop DURATION=0
|
||||
|
||||
{% elif TARGET >= THRESHOLD %}
|
||||
# Heating above threshold
|
||||
BEDFANSSLOW
|
||||
UPDATE_DELAYED_GCODE ID=bedfanloop DURATION=1
|
||||
|
||||
{% else %}
|
||||
# Low temp request
|
||||
BEDFANSOFF
|
||||
UPDATE_DELAYED_GCODE ID=bedfanloop DURATION=0
|
||||
{% endif %}
|
||||
# --- END NEW LOGIC ---
|
||||
|
||||
{% else %}
|
||||
# {action_respond_info("(Bedfans macro)Heater %s not supported (trying classic _SET_HEATER_TEMPERATURE)" % HEATER)}
|
||||
_SET_HEATER_TEMPERATURE {rawparams}
|
||||
{% endif %}
|
||||
|
||||
# Override M190 (Wait for Bed Temperature)
|
||||
# As a bonus, use TEMPERATURE_WAIT so we don't have to wait for PID to level off.
|
||||
[gcode_macro M190]
|
||||
rename_existing: M99190
|
||||
gcode:
|
||||
# Parameters
|
||||
{% set S = params.S|int %}
|
||||
# Vars
|
||||
{% set THRESHOLD = printer["gcode_macro _BEDFANVARS"].threshold|int %}
|
||||
|
||||
{% if S >= THRESHOLD %}
|
||||
BEDFANSSLOW # >= Threshold temp: Low speed fans while heating
|
||||
{% else %}
|
||||
BEDFANSOFF # < Threshold temp: Turn bed fans off
|
||||
{% endif %}
|
||||
|
||||
M140 {% for p in params
|
||||
%}{'%s%s' % (p, params[p])}{%
|
||||
endfor %} # Set bed temp
|
||||
|
||||
{% if S != 0 %}
|
||||
TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={S|int} MAXIMUM={S|int + 5} # Wait for bed temp within 5 degrees
|
||||
{% endif %}
|
||||
|
||||
# Post-heating fan speeds
|
||||
{% if S >= THRESHOLD %}
|
||||
BEDFANSFAST # >= Threshold temp: Higher speed fans after heating finished
|
||||
{% endif %}
|
||||
|
||||
# Replace M140 (Set Bed Temperature) to just be an alias of SET_HEATER_TEMPERATURE (which has associated bed fan logic if enabled)
|
||||
[gcode_macro M140]
|
||||
rename_existing: M99140
|
||||
gcode:
|
||||
# Parameters
|
||||
{% set S = params.S|float %}
|
||||
|
||||
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={S}
|
||||
|
||||
# Replace TURN_OFF_HEATERS
|
||||
# [gcode_macro TURN_OFF_HEATERS]
|
||||
# rename_existing: _TURN_OFF_HEATERS
|
||||
# gcode:
|
||||
# BEDFANSOFF
|
||||
# _TURN_OFF_HEATERS
|
||||
[gcode_macro TURN_OFF_HEATERS]
|
||||
rename_existing: _TURN_OFF_HEATERS
|
||||
gcode:
|
||||
# Vars
|
||||
{% set COOLDOWN = printer["gcode_macro _BEDFANVARS"].cooldown_temp|int %}
|
||||
|
||||
# Only keep fans running if bed is still hot
|
||||
{% if printer.heater_bed.temperature|int > COOLDOWN %}
|
||||
BEDFANSSLOW
|
||||
UPDATE_DELAYED_GCODE ID=bedfancooldown DURATION=5
|
||||
{% else %}
|
||||
BEDFANSOFF
|
||||
{% endif %}
|
||||
|
||||
_TURN_OFF_HEATERS
|
||||
|
||||
################ Monitoring loop #####################
|
||||
|
||||
# Turns bed fans to "fast" speed once target bed temp is reached.
|
||||
[delayed_gcode bedfanloop]
|
||||
gcode:
|
||||
# Vars
|
||||
{% set THRESHOLD = printer["gcode_macro _BEDFANVARS"].threshold|int %}
|
||||
|
||||
{% if printer.heater_bed.target >= THRESHOLD %} # Continue only if target temp greater than threshold.
|
||||
{% if printer.heater_bed.temperature|int >= (printer.heater_bed.target|int - 1) %}
|
||||
BEDFANSFAST # If within 1 degree of target temp: Higher speed fans
|
||||
{% else %}
|
||||
UPDATE_DELAYED_GCODE ID=bedfanloop DURATION=5 # If temp not reached yet: loop again
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
[delayed_gcode bedfancooldown]
|
||||
gcode:
|
||||
# Vars
|
||||
{% set COOLDOWN = printer["gcode_macro _BEDFANVARS"].cooldown_temp|int %}
|
||||
|
||||
{% if printer.heater_bed.temperature|int > COOLDOWN %}
|
||||
BEDFANSSLOW
|
||||
UPDATE_DELAYED_GCODE ID=bedfancooldown DURATION=10
|
||||
{% else %}
|
||||
BEDFANSOFF
|
||||
{% endif %}
|
||||
@@ -0,0 +1,300 @@
|
||||
###########################################################
|
||||
# Filament Drying #
|
||||
###########################################################
|
||||
# #
|
||||
# Macro Settings #
|
||||
# Z-axis bed movement - value in mm #
|
||||
# Chamber with chamber heater - value in degrees Celsius #
|
||||
# Bed - value in degrees Celsius #
|
||||
# Drying duration - value in minutes #
|
||||
# Recirculation fan - value in % #
|
||||
# Recirculation runtime - value in minutes #
|
||||
# Recirculation wait time - value in minutes #
|
||||
# Exhaust fan - value in % #
|
||||
# Exhaust runtime - value in minutes #
|
||||
# Exhaust wait time - value in minutes #
|
||||
# Setting run and wait times to "0" will keep #
|
||||
# the corresponding fan running continuously #
|
||||
# #
|
||||
###########################################################
|
||||
[gcode_macro START_DRYING]
|
||||
description: Start the filament dryer for the heated bed.
|
||||
gcode:
|
||||
# Drying parameters
|
||||
{% set MaxChamberTemperature = 60.0 %}
|
||||
{% set MaxBedTemperature = 100.0 %}
|
||||
|
||||
BEDFANSFAST
|
||||
|
||||
# Check the chamber and bed temperatures
|
||||
{% set Chamber = params.CHAMBER|default(25.0)|float %}
|
||||
{% set Bed = params.BED|default(45.0)|float %}
|
||||
|
||||
{% if Chamber < 0 %}
|
||||
M118 Error: Chamber temperature cannot be negative.
|
||||
M117 Error: Chamber temperature negative.
|
||||
{% set SkipRemainingCommands = True %}
|
||||
{% elif Chamber > MaxChamberTemperature %}
|
||||
M118 Warning: Chamber temperature exceeds the maximum value of { MaxChamberTemperature }°C. Drying will be stopped.
|
||||
M117 Warning: Chamber temperature exceeds the maximum value of { MaxChamberTemperature }°C. Drying will be stopped.
|
||||
{% set SkipRemainingCommands = True %}
|
||||
{% else %}
|
||||
{% set SkipRemainingCommands = False %}
|
||||
{% endif %}
|
||||
|
||||
{% if not SkipRemainingCommands %}
|
||||
{% if Bed < 0 %}
|
||||
M118 Error: Bed temperature cannot be negative.
|
||||
M117 Error: Bed temperature negative.
|
||||
{% set SkipRemainingCommands = True %}
|
||||
{% elif Bed == 0 %}
|
||||
M118 Error: Bed temperature is 0 degrees. How will drying be achieved?
|
||||
M117 Error: Bed temperature is 0 degrees. How will drying be achieved?
|
||||
{% set SkipRemainingCommands = True %}
|
||||
{% elif Bed > MaxBedTemperature %}
|
||||
M118 Warning: Bed temperature exceeds the maximum value of { MaxBedTemperature }°C. Drying will be stopped.
|
||||
M117 Warning: Bed temperature exceeds the maximum value of { MaxBedTemperature }°C. Drying will be stopped.
|
||||
{% set SkipRemainingCommands = True %}
|
||||
{% else %}
|
||||
{% set SkipRemainingCommands = False %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
# Check exhaust and recirculation fan values
|
||||
{% set Exhaust = params.EXHAUST|default(100)|int %}
|
||||
{% set Recirculation = params.RECIRCULATION|default(100)|int %}
|
||||
|
||||
{% if Exhaust < 0 %}
|
||||
M118 Error: Exhaust value cannot be negative.
|
||||
M117 Error: Exhaust value negative.
|
||||
{% set SkipRemainingCommands = True %}
|
||||
{% elif Recirculation < 0 %}
|
||||
M118 Error: Recirculation value cannot be negative.
|
||||
M117 Error: Recirculation value negative.
|
||||
{% set SkipRemainingCommands = True %}
|
||||
{% elif Exhaust > 100 %}
|
||||
M118 Warning: Exhaust value exceeds the maximum value of 100%. Value will be limited to 100%.
|
||||
M117 Warning: Exhaust value exceeds the maximum value of 100%. Value will be limited to 100%.
|
||||
{% set Exhaust = 100 %}
|
||||
{% elif Recirculation > 100 %}
|
||||
M118 Warning: Recirculation value exceeds the maximum value of 100%. Value will be limited to 100%.
|
||||
M117 Warning: Recirculation value exceeds the maximum value of 100%. Value will be limited to 100%.
|
||||
{% set Recirculation = 100 %}
|
||||
{% endif %}
|
||||
|
||||
{% if not SkipRemainingCommands %}
|
||||
; Enter drying duration in minutes, limited to a maximum of 12 hours (720 minutes)
|
||||
{% set DryingDurationMinutes = params.DRYING_DURATION|default(240)|int %}
|
||||
{% if DryingDurationMinutes > 720 %}
|
||||
{% set DryingDurationMinutes = 720 %}
|
||||
M118 Warning: Drying duration exceeds 12 hours. Duration limited to 12 hours.
|
||||
M117 Warning: Drying duration exceeds 12 hours. Duration limited to 12 hours.
|
||||
{% endif %}
|
||||
{% set DryingDuration = DryingDurationMinutes * 60 %}
|
||||
|
||||
# Recirculation and exhaust fan settings
|
||||
{% set WaitTime_ExhaustMinutes = params.WAIT_TIME_EXHAUST|default(60)|int %}
|
||||
{% set Runtime_ExhaustMinutes = params.RUNTIME_EXHAUST|default(30)|int %}
|
||||
{% set WaitTime_RecirculationMinutes = params.WAIT_TIME_RECIRCULATION|default(60)|int %}
|
||||
{% set Runtime_RecirculationMinutes = params.RUNTIME_RECIRCULATION|default(30)|int %}
|
||||
|
||||
# Check wait and runtime settings
|
||||
{% if WaitTime_ExhaustMinutes < 0 or Runtime_ExhaustMinutes < 0 or WaitTime_RecirculationMinutes < 0 or Runtime_RecirculationMinutes < 0 %}
|
||||
M118 Error: Wait and runtime settings cannot be negative.
|
||||
M117 Error: One or more wait or runtime settings are negative.
|
||||
{% set SkipRemainingCommands = True %}
|
||||
{% endif %}
|
||||
|
||||
{% if not SkipRemainingCommands %}
|
||||
# Convert wait and runtime to seconds
|
||||
{% set WaitTime_Exhaust = WaitTime_ExhaustMinutes * 60 %}
|
||||
{% set Runtime_Exhaust = Runtime_ExhaustMinutes * 60 %}
|
||||
{% set WaitTime_Recirculation = WaitTime_RecirculationMinutes * 60 %}
|
||||
{% set Runtime_Recirculation = Runtime_RecirculationMinutes * 60 %}
|
||||
|
||||
# Convert percentage to PWM value (0-255)
|
||||
{% set ExhaustPWM = (Exhaust * 255 / 100)|int %}
|
||||
{% set RecirculationPWM = (Recirculation * 255 / 100)|int %}
|
||||
|
||||
# Ensure PWM values are within the range (0-255)
|
||||
{% if ExhaustPWM > 255 %}
|
||||
{% set ExhaustPWM = 255 %}
|
||||
{% elif ExhaustPWM < 0 %}
|
||||
{% set ExhaustPWM = 0 %}
|
||||
{% endif %}
|
||||
|
||||
{% if RecirculationPWM > 255 %}
|
||||
{% set RecirculationPWM = 255 %}
|
||||
{% elif RecirculationPWM < 0 %}
|
||||
{% set RecirculationPWM = 0 %}
|
||||
{% endif %}
|
||||
|
||||
# Issue fan speed warnings if speed is 0
|
||||
{% if Exhaust == 0 and (WaitTime_Exhaust > 0 or Runtime_Exhaust > 0) %}
|
||||
M118 Warning: Exhaust fan speed is 0, but wait time or runtime specified.
|
||||
M117 Warning: Exhaust fan speed is 0, but wait time or runtime specified.
|
||||
{% endif %}
|
||||
|
||||
{% if Recirculation == 0 and (WaitTime_Recirculation > 0 or Runtime_Recirculation > 0) %}
|
||||
M118 Warning: Recirculation fan speed is 0, but wait time or runtime specified.
|
||||
M117 Warning: Recirculation fan speed is 0, but wait time or runtime specified.
|
||||
{% endif %}
|
||||
|
||||
# Check if fans should run continuously
|
||||
{% set ExhaustPermanent = (WaitTime_Exhaust == 0 and Runtime_Exhaust == 0) %}
|
||||
{% set RecirculationPermanent = (WaitTime_Recirculation == 0 and Runtime_Recirculation == 0) %}
|
||||
|
||||
# Check homing status of the toolhead
|
||||
{% if printer.toolhead.homed_axes|default("") != "xyz" %}
|
||||
M118 Printer is homing!
|
||||
M117 Printer is homing!
|
||||
G28 ; Home all axes
|
||||
M118 Homing completed
|
||||
M117 Homing completed
|
||||
{% else %}
|
||||
M118 Homing already completed!
|
||||
M117 Homing already completed!
|
||||
{% endif %}
|
||||
|
||||
G1 X175 Y175 F3000 ; Move the toolhead quickly to X=1, Y=1
|
||||
|
||||
# Z-axis bed movement
|
||||
{% set Z_movement = params.Z_MOVEMENT|default(250.0)|float %}
|
||||
{% if Z_movement > 0 %}
|
||||
G1 Z+{Z_movement} F1500 ; Move the bed up in the Z-axis at the specified speed
|
||||
G1 X175 Y350 Z+{Z_movement} F1500
|
||||
M118 Bed moved by Z={Z_movement}
|
||||
M117 Bed moved by Z={Z_movement}
|
||||
{% endif %}
|
||||
|
||||
# Wait until the Z-displacement is complete
|
||||
M118 Wait for completion of Z-displacement...
|
||||
M117 Wait for completion of Z-displacement...
|
||||
|
||||
# Repeat M114 to ensure that the Z position is reached
|
||||
M114 # Query the current position
|
||||
G4 P1000 # Short pause to query the Z position
|
||||
|
||||
# When Z-movement is complete, start countdown
|
||||
M118 Z-displacement completed.
|
||||
M117 Z-displacement completed.
|
||||
# Wait 30 seconds
|
||||
M118 Please insert filament and close the door! Drying starts in 30 seconds.
|
||||
M117 Please insert filament and close the door! Drying starts in 30 seconds.
|
||||
G4 P5000 # Wait 5 seconds
|
||||
# Loop for the countdown from 25 seconds to 5 seconds
|
||||
{% for i in range(1, 6) %}
|
||||
M118 Drying in {25 - 5 * (i - 1)} seconds!
|
||||
M117 Drying in {25 - 5 * (i - 1)} seconds!
|
||||
G4 P5000 # wait 5 seconds
|
||||
{% endfor %}
|
||||
|
||||
# Turn on heating elements
|
||||
M140 S{ Bed } ; Set bed temperature without waiting.
|
||||
M141 S{ Chamber } ; [OPTIONAL] Set chamber temperature.
|
||||
|
||||
# Set dryer status and start timer
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=time_remaining VALUE={ DryingDuration }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=bed_temperature VALUE={ Bed }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=chamber_temperature VALUE={ Chamber }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=exhaust VALUE={ Exhaust }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=exhaust_pwm VALUE={ ExhaustPWM }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=recirculation VALUE={ Recirculation }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=recirculation_pwm VALUE={ RecirculationPWM }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=waittime_exhaust VALUE={ WaitTime_Exhaust }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=runtime_exhaust VALUE={ Runtime_Exhaust }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=waittime_recirculation VALUE={ WaitTime_Recirculation }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=runtime_recirculation VALUE={ Runtime_Recirculation }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=exhaust_permanent VALUE={ ExhaustPermanent }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=recirculation_permanent VALUE={ RecirculationPermanent }
|
||||
|
||||
# Start the delayed gcode for timer
|
||||
UPDATE_DELAYED_GCODE ID=DRYING_TIMER DURATION=1
|
||||
|
||||
M118 Drying in progress...
|
||||
M117 Drying in progress...
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro STOP_DRYING]
|
||||
description: Stops the filament drying process
|
||||
gcode:
|
||||
# Turn off heating elements and fans
|
||||
M140 S0 ; Turn off heating element
|
||||
M141 S0 ; [OPTIONAL] Turn off chamber heater/fan
|
||||
M106 P2 S0 ; Turn off recirculation fan
|
||||
M106 P3 S0 ; Turn off exhaust fan
|
||||
|
||||
# Stop timer and update status
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=time_remaining VALUE=0
|
||||
UPDATE_DELAYED_GCODE ID=DRYING_TIMER DURATION=0
|
||||
M117 Drying stopped! Please remove filament.
|
||||
M118 Drying stopped! Please remove filament.
|
||||
|
||||
# Query print bed temperature
|
||||
{% if printer.heater_bed.temperature > 40 %}
|
||||
M117 Attention: Print bed above 40°C! Take care when removing the filament roll.
|
||||
M118 Attention: Print bed above 40°C! Caution when removing the filament roll.
|
||||
{% else %}
|
||||
M117 Print bed temperature OK.
|
||||
M118 Print bed temperature OK.
|
||||
{% endif %}
|
||||
|
||||
# Send Telegram notification
|
||||
#RESPOND PREFIX=tgnotify MSG="Filament drying has been stopped. Please remove the filament."
|
||||
|
||||
[gcode_macro STATUS_DRYING]
|
||||
variable_time_remaining: 0
|
||||
variable_bed_temperature: 0
|
||||
variable_chamber_temperature: 0
|
||||
variable_exhaust: 100
|
||||
variable_exhaust_pwm: 255
|
||||
variable_recirculation: 100
|
||||
variable_recirculation_pwm: 255
|
||||
variable_waittime_exhaust: 60
|
||||
variable_runtime_exhaust: 30
|
||||
variable_waittime_recirculation: 60
|
||||
variable_runtime_recirculation: 30
|
||||
variable_exhaust_permanent: False
|
||||
variable_recirculation_permanent: False
|
||||
gcode:
|
||||
{% set cycle_time_exhaust = waittime_exhaust + runtime_exhaust %}
|
||||
{% set cycle_time_recirculation = waittime_recirculation + runtime_recirculation %}
|
||||
{% if time_remaining > 0 %}
|
||||
{% set current_time = time_remaining - 1 %}
|
||||
|
||||
{% set hours = (current_time // 3600) %}
|
||||
{% set minutes = (current_time % 3600) // 60 %}
|
||||
{% set seconds = (current_time % 3600) % 60 %}
|
||||
|
||||
{% set time_display = "%02d:%02d:%02d" % (hours, minutes, seconds) %}
|
||||
|
||||
# Update fan states based on the current cycle
|
||||
{% if exhaust_permanent or (current_time % cycle_time_exhaust) < runtime_exhaust %}
|
||||
; Exhaust fan running
|
||||
M106 P3 S{ exhaust_pwm }
|
||||
{% else %}
|
||||
; Exhaust fan off
|
||||
M106 P3 S0
|
||||
{% endif %}
|
||||
|
||||
{% if recirculation_permanent or (current_time % cycle_time_recirculation) < runtime_recirculation %}
|
||||
# Recirculation fan running
|
||||
M106 P2 S{ recirculation_pwm }
|
||||
{% else %}
|
||||
# Recirculation fan off
|
||||
M106 P2 S0
|
||||
{% endif %}
|
||||
|
||||
M117 Remaining time: { time_display }
|
||||
SET_GCODE_VARIABLE MACRO=STATUS_DRYING VARIABLE=time_remaining VALUE={ current_time }
|
||||
{% else %}
|
||||
# Stop drying when time runs out
|
||||
STOP_DRYING
|
||||
{% endif %}
|
||||
|
||||
[delayed_gcode DRYING_TIMER]
|
||||
gcode:
|
||||
UPDATE_DELAYED_GCODE ID=DRYING_TIMER DURATION=1
|
||||
STATUS_DRYING
|
||||
@@ -0,0 +1,301 @@
|
||||
##
|
||||
## HEAT_SOAK HEATER=<heater_name> TARGET=<target_heater_temperature> SOAKER=<heat_soak_temp_sensor_name> [RATE=<heat_soak_temp_rate_of_change>] [TIMEOUT=<time_to_abort_heat_soak>]
|
||||
##
|
||||
##
|
||||
## e.g.
|
||||
## HEAT_SOAK HEATER=heater_bed TARGET=100 SOAKER="temperature_sensor top_bed"
|
||||
##
|
||||
## Based on work by blalor: https://klipper.discourse.group/t/interruptible-heat-soak/1552
|
||||
##
|
||||
## Modified by me
|
||||
##
|
||||
|
||||
[gcode_macro HEAT_SOAK]
|
||||
description: Wait for a temperature sensor to reach thermal equilibrium
|
||||
|
||||
variable_last_temp: 0
|
||||
variable_last_soak_temp_rate: None
|
||||
variable_temp_history: None
|
||||
variable_smoothed_temp_history: None
|
||||
variable_stage: "done" ## heating -> soaking -> done -> None
|
||||
variable_total_time_elapsed: 0
|
||||
variable_soak_time_remaining: 30
|
||||
variable_check_interval: 1
|
||||
variable_heating_report_interval: 2
|
||||
variable_soaking_report_interval: 5
|
||||
variable_heater_sensor: None
|
||||
variable_soaker_sensor: None
|
||||
variable_target_temp: 0.0
|
||||
variable_min_soak_temp: 0
|
||||
variable_target_rate: 0.3
|
||||
variable_complete: None
|
||||
variable_cancel: None
|
||||
variable_temp_smooth_time: 4.0
|
||||
variable_rate_smooth_time: 20.0
|
||||
variable_resume_trigger: False
|
||||
variable_was_print_active: False
|
||||
|
||||
gcode:
|
||||
{ action_respond_info( "Heat Soak starting" )}
|
||||
|
||||
# Soaker is required
|
||||
{% set SOAKER = params.SOAKER | string %}
|
||||
{% set RATE = params.RATE | default(0.3) | float %} ## in degrees C per minute
|
||||
{% set SOAK_TEMP = params.SOAK_TEMP | default(0.0) | float %}
|
||||
{% set HEATER = (params.HEATER | string) %} # Optional heater
|
||||
{% set TARGET = params.TARGET | default(0) | float %}
|
||||
{% set TIMEOUT = (params.TIMEOUT | default(30) | int) * 60 %} ## minutes to seconds
|
||||
{% set TEMP_SMOOTH = params.TEMP_SMOOTH | default(4.0) | float %} ## seconds for temp smoothing
|
||||
{% set RATE_SMOOTH = params.RATE_SMOOTH | default(20) | float %} ## seconds for rate smoothing
|
||||
{% set COMPLETE = (params.COMPLETE | string) %}
|
||||
{% set CANCEL = (params.CANCEL | string) %}
|
||||
{% set HEATING_REPORT_INTERVAL = params.HEATING_REPORT_INTERVAL | default(2) | int %}
|
||||
{% set SOAKING_REPORT_INTERVAL = params.SOAKING_REPORT_INTERVAL | default(5) | int %}
|
||||
|
||||
# User writeable variables
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=heater_sensor VALUE="{HEATER | pprint}"
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_temp VALUE={TARGET}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=min_soak_temp VALUE={SOAK_TEMP}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soaker_sensor VALUE="{SOAKER | pprint}"
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=target_rate VALUE={RATE}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={TIMEOUT}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=complete VALUE="'{COMPLETE}'"
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=cancel VALUE="'{CANCEL}'"
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=temp_smooth_time VALUE={TEMP_SMOOTH}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=rate_smooth_time VALUE={RATE_SMOOTH}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=heating_report_interval VALUE={HEATING_REPORT_INTERVAL}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soaking_report_interval VALUE={SOAKING_REPORT_INTERVAL}
|
||||
|
||||
# Internal variables
|
||||
{% set soak_temp = printer[SOAKER].temperature %}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=last_temp VALUE={soak_temp}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=temp_history VALUE="{[soak_temp] | pprint}"
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=smoothed_temp_history VALUE="{[] | pprint}"
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE=0
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=last_soak_temp_rate VALUE=None
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=check_interval VALUE=1.0
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=resume_trigger VALUE=False
|
||||
{% set is_print_active = printer['virtual_sdcard'].is_active or printer['virtual_sdcard'].file_position != 0.0 %}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=was_print_active VALUE={is_print_active}
|
||||
|
||||
# start optional heater
|
||||
{% if (HEATER and TARGET != 0.0) %}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'heating'"
|
||||
SET_HEATER_TEMPERATURE HEATER={HEATER} TARGET={TARGET}
|
||||
{% else %}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'soaking'"
|
||||
{% endif %}
|
||||
|
||||
# pause the print, if active
|
||||
{% if is_print_active %}
|
||||
PAUSE_BASE
|
||||
{% endif %}
|
||||
|
||||
UPDATE_DELAYED_GCODE ID=_heat_soaker DURATION={check_interval}
|
||||
|
||||
[gcode_macro STOP_HEAT_SOAK]
|
||||
description: stops heat soak activity without running any callbacks
|
||||
gcode:
|
||||
UPDATE_DELAYED_GCODE ID=_heat_soaker DURATION=0 # cancel any pending run
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'done'"
|
||||
|
||||
[gcode_macro CANCEL_HEAT_SOAK]
|
||||
description: cancels an in-progress HEAT_SOAK cycle and runs the cancel callback
|
||||
gcode:
|
||||
# check that the HEAT_SOAK macro is active before canceling
|
||||
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
|
||||
{% set stage = heat_soak.stage %}
|
||||
{% if stage in ("heating", "soaking") %}
|
||||
STOP_HEAT_SOAK
|
||||
{% if heat_soak.cancel %}
|
||||
{heat_soak.cancel}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro HEAT_SOAK_RESUME]
|
||||
description: Resume while heat soaking results in the soaking phase being skipped, any complete callback is run
|
||||
gcode:
|
||||
# {% set ON_RESUME = (params.ON_RESUME | default("_HEAT_SOAK__BASE_RESUME")) | string %}
|
||||
{% set ON_RESUME = (params.ON_RESUME | default("_HEAT_SOAK__BASE_RESUME")) | string %}
|
||||
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
|
||||
{% set stage = heat_soak.stage %}
|
||||
{% if stage == "heating" %}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=resume_trigger VALUE=True
|
||||
{% elif stage == "soaking" %}
|
||||
STOP_HEAT_SOAK
|
||||
{% if heat_soak.complete %}
|
||||
{heat_soak.complete}
|
||||
{% endif %}
|
||||
{ON_RESUME} {% for p in params %}{'%s=%s ' % (p, params[p])}{% endfor %}
|
||||
{% else %}
|
||||
{ON_RESUME} {% for p in params %}{'%s=%s ' % (p, params[p])}{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro RESUME]
|
||||
description: Resume while heat soaking results in the soaking phase being skipped, any complete callback is run
|
||||
rename_existing: _HEAT_SOAK__BASE_RESUME
|
||||
gcode:
|
||||
HEAT_SOAK_RESUME ON_RESUME=_HEAT_SOAK__BASE_RESUME
|
||||
|
||||
[delayed_gcode _heat_soaker]
|
||||
# description: internal macro to handle heat soaking activity
|
||||
gcode:
|
||||
{% set heat_soak = printer['gcode_macro HEAT_SOAK'] %}
|
||||
# debug
|
||||
#{ action_respond_info( printer['gcode_macro HEAT_SOAK'] | tojson )}
|
||||
|
||||
# shut down cleanly if we detect that the user canceled or started a print while heat soaking
|
||||
{% set file_position_is_zero = (printer['virtual_sdcard'].file_position == 0.0 )%}
|
||||
{% if heat_soak.was_print_active and file_position_is_zero %}
|
||||
STOP_HEAT_SOAK
|
||||
{action_respond_info("HEAT_SOAK aborted. Detected a change in virtual_sdcard print state. Call STOP_HEAT_SOAK from CANCEL_PRINT to stop seeing this error message.")}
|
||||
{% endif %}
|
||||
|
||||
# shut down cleanly if we detect that the user resumed the print while heat soaking
|
||||
{% if not file_position_is_zero and not printer['pause_resume'].is_paused %}
|
||||
STOP_HEAT_SOAK
|
||||
{action_respond_info("HEAT_SOAK aborted. Print is no longer paused. Call STOP_HEAT_SOAK or HEAT_SOAK_RESUME from your RESUME macro to stop seeing this error message.")}
|
||||
{% endif %}
|
||||
|
||||
# update total time elapsed
|
||||
{% set total_time_elapsed = heat_soak.total_time_elapsed + heat_soak.check_interval %}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE={ total_time_elapsed }
|
||||
|
||||
{% set soaker_sensor = printer[heat_soak.soaker_sensor] %}
|
||||
{% set soak_temp = soaker_sensor.temperature %}
|
||||
{% if heat_soak.heater_sensor %}
|
||||
{% set heater_sensor = printer[heat_soak.heater_sensor] %}
|
||||
{% set heater_temp = heater_sensor.temperature | round(1) %}
|
||||
{% endif %}
|
||||
{% set stage = heat_soak.stage %}
|
||||
|
||||
# compute the new temp difference and save history
|
||||
{% set temp_history = heat_soak.temp_history %}
|
||||
{% set smoothed_temp_history = heat_soak.smoothed_temp_history %}
|
||||
{% set temp_diff = soak_temp - (temp_history | last) %}
|
||||
{% set _ = temp_history.append(soak_temp) %}
|
||||
{% set soak_temp_smoothed = None %}
|
||||
{% set slope = None %}
|
||||
{% if (temp_history | length) > heat_soak.temp_smooth_time %}
|
||||
{% set temp_history = temp_history[1:] %}
|
||||
# compute average temp
|
||||
{% set soak_temp_smoothed = ((temp_history | sum) / (temp_history | length)) %}
|
||||
# save raw value to history
|
||||
{% set _ = smoothed_temp_history.append(soak_temp_smoothed) %}
|
||||
# round off for display and comparisons later
|
||||
{% set soak_temp_smoothed = soak_temp_smoothed | round(3) %}
|
||||
# compute the slope
|
||||
{% if (smoothed_temp_history | length) > heat_soak.rate_smooth_time %}
|
||||
{% set smoothed_temp_history = smoothed_temp_history[1:] %}
|
||||
# Least Squares
|
||||
# adapted from: https://towardsdatascience.com/linear-regression-using-least-squares-a4c3456e8570
|
||||
# X values are time in seconds
|
||||
# Y values are temperatures
|
||||
{% set count = (smoothed_temp_history | length) %}
|
||||
{% set times = range(0, count, 1) %}
|
||||
{% set x_sum = (times | sum) | float %}
|
||||
{% set y_sum = (smoothed_temp_history | sum) | float %}
|
||||
# Squares
|
||||
# Jinja oddity: += doesn't work in loops??, so collect squares and sum after
|
||||
{% set xx_sum_arr = [] %}
|
||||
{% set xy_sum_arr = [] %}
|
||||
{% for i in times %}
|
||||
{% set x = times[i] %}
|
||||
{% set y = smoothed_temp_history[i] %}
|
||||
{% set _ = xx_sum_arr.append(x * x) %}
|
||||
{% set _ = xy_sum_arr.append(x * y) %}
|
||||
{% endfor %}
|
||||
{% set xx_sum = (xx_sum_arr | sum) | float %}
|
||||
{% set xy_sum = (xy_sum_arr | sum) | float %}
|
||||
# Slope calculation. Slope is per second so * 60 for rate per minute
|
||||
{% set slope = (60.0 * ((count | float * xy_sum) - (x_sum * y_sum)) / ((count | float * xx_sum) - (x_sum * x_sum))) | round(3) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=temp_history VALUE="{(temp_history | pprint | replace("\n", ""))}"
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=smoothed_temp_history VALUE="{(smoothed_temp_history | pprint | replace("\n", ""))}"
|
||||
|
||||
# while heating
|
||||
{% if stage == "heating" %}
|
||||
{% if heater_temp < heat_soak.target_temp %}
|
||||
{% if total_time_elapsed % heat_soak.heating_report_interval == 0 %}
|
||||
{% set message = "Heating -- %.1fC / %.1fC -- %.1fm elapsed" % (heater_temp, heat_soak.target_temp, total_time_elapsed / 60.0) %}
|
||||
M117 {message}
|
||||
{action_respond_info(message)}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{action_respond_info("Heating completed after ~%.1fm, starting heat soak phase." % (total_time_elapsed / 60.0))}
|
||||
# skip the soak phase if resume was pressed
|
||||
{% set stage = "done" if heat_soak.resume_trigger else "soaking" %}
|
||||
# reset total time to 0 so soaking time is reported from 0
|
||||
{% set total_time_elapsed = 0 %}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=total_time_elapsed VALUE={ total_time_elapsed }
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
# while soaking
|
||||
{% if stage == "soaking" %}
|
||||
## decrement the soak countdown
|
||||
{% set soak_time_remaining = [heat_soak.soak_time_remaining - heat_soak.check_interval, 0] | max %}
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=soak_time_remaining VALUE={ soak_time_remaining }
|
||||
|
||||
## abort soaking if timeout is reached
|
||||
{% if soak_time_remaining <= 0 %}
|
||||
{action_respond_info("Heat soak timed out after ~%.1fm" % (total_time_elapsed / 60.0))}
|
||||
{% set stage = "cancel" %}
|
||||
# if the smoothing algorithm hasn't settled on these values yet, dont evaluate them
|
||||
{% elif slope is none or soak_temp_smoothed is none %}
|
||||
{% if total_time_elapsed % heat_soak.soaking_report_interval == 0 %}
|
||||
## not enough data points yet to determing the soak rate, show dashes
|
||||
{% set message = "Soaking -- gathering data -- %.1fm remaining" % (soak_time_remaining / 60.0) %}
|
||||
M117 { message }
|
||||
{action_respond_info(message)}
|
||||
{% endif %}
|
||||
# if the heat soak must continue for some reason
|
||||
{% elif (slope > heat_soak.target_rate) or (heat_soak.min_soak_temp > 0 and soak_temp_smoothed < heat_soak.min_soak_temp) %}
|
||||
{% if total_time_elapsed % heat_soak.soaking_report_interval == 0 %}
|
||||
{% set format_vars = [
|
||||
soak_temp_smoothed | float,
|
||||
(heat_soak.min_soak_temp | float | round(0) | string) if (heat_soak.min_soak_temp > 0) else "--",
|
||||
slope,
|
||||
heat_soak.target_rate,
|
||||
soak_time_remaining / 60.0
|
||||
] %}
|
||||
{% set message = "Soaking -- temp: {0:.1f}C / {1}C -- rate: {2:.3f}C/m / {3:.3f}C/m -- {4:.1f}m remaining".format(*format_vars) %}
|
||||
M117 { message }
|
||||
{action_respond_info(message)}
|
||||
{% endif %}
|
||||
## end soaking if the target rate is achieved
|
||||
{% else %}
|
||||
{action_respond_info("Heat soak complete after ~%.1fm at %.3fC/m / %.1fC" % (total_time_elapsed / 60.0, slope, soak_temp))}
|
||||
{% set stage = "done" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
# save stage before calling any macros
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'{stage}'"
|
||||
|
||||
{% if stage in ("done", "cancel") %}
|
||||
STOP_HEAT_SOAK
|
||||
{% if stage == "cancel" %}
|
||||
{% if heat_soak.cancel %}
|
||||
{heat_soak.cancel}
|
||||
{% elif printer['virtual_sdcard'].is_active %}
|
||||
CANCEL_PRINT
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if heat_soak.complete %}
|
||||
{heat_soak.complete}
|
||||
{% endif %}
|
||||
{% if printer['pause_resume'].is_paused %}
|
||||
RESUME
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
## trigger ourselves again
|
||||
UPDATE_DELAYED_GCODE ID=_heat_soaker DURATION={ heat_soak.check_interval }
|
||||
## dwell for 1ms to prevent from going idle
|
||||
G4 P1
|
||||
{% endif %}
|
||||
|
||||
# save ending value of stage
|
||||
SET_GCODE_VARIABLE MACRO=HEAT_SOAK VARIABLE=stage VALUE="'{stage}'"
|
||||
@@ -0,0 +1,259 @@
|
||||
#========================================================
|
||||
#=====================iHeater cfg========================
|
||||
#========================================================
|
||||
|
||||
#=====================iHeater pins=======================
|
||||
[board_pins iHeater_Aliases]
|
||||
mcu: iHeater
|
||||
aliases:
|
||||
TH0=PA3, TH1=PA0, TH2=PB1,
|
||||
HEATER=PA1, FAN=PA2,
|
||||
MODE=PA4,
|
||||
LED1=PA7, LED2=PA6, LED3=PA5,
|
||||
USART1_TX=PA9, USART1_RX=PA10
|
||||
|
||||
#=====================iHeater mcu=======================
|
||||
# [mcu]
|
||||
# serial: /dev/serial/by-id/usb-Klipper_stm32f042x6_0C0018000D53304347373020-if00
|
||||
|
||||
[mcu iHeater] #second MCU
|
||||
#is_non_critical: true: true # flag this as non critical
|
||||
serial: /dev/serial/by-id/usb-Klipper_stm32f042x6_0C0020000E43304754393320-if00
|
||||
restart_method: command
|
||||
|
||||
[temperature_sensor iHeater]
|
||||
sensor_type: temperature_mcu
|
||||
sensor_mcu: iHeater
|
||||
|
||||
#============================================================
|
||||
#======================= iHeater CONFIG ======================
|
||||
#============================================================
|
||||
|
||||
# Make sure to perform PID calibration for the chamber heater:
|
||||
#🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||
# PID_CALIBRATE HEATER=iHeater_H TARGET=70
|
||||
#🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥
|
||||
# This is essential for accurate and stable chamber heating.
|
||||
# Without PID tuning, the temperature may overshoot or take too long to stabilize!
|
||||
|
||||
#===========================================================
|
||||
|
||||
[heater_generic iHeater_H]
|
||||
heater_pin: iHeater:HEATER
|
||||
max_power: 1
|
||||
sensor_type: NTC 100K MGB18-104F39050L32
|
||||
sensor_pin: iHeater:TH0
|
||||
pwm_cycle_time: 0.3
|
||||
min_temp: 0
|
||||
max_temp: 120
|
||||
control: pid
|
||||
# pid_Kp: 37.948
|
||||
# pid_Ki: 2.040
|
||||
# pid_Kd: 176.497
|
||||
pid_Kp: 26.656
|
||||
pid_Ki: 0.96
|
||||
pid_Kd: 184.971
|
||||
|
||||
[verify_heater iHeater_H]
|
||||
max_error: 240
|
||||
check_gain_time: 1800
|
||||
heating_gain: 1
|
||||
|
||||
[heater_fan chamber_heater_fan]
|
||||
pin: iHeater:FAN
|
||||
heater: iHeater_H
|
||||
heater_temp: 50.0
|
||||
fan_speed: 1.0
|
||||
shutdown_speed: 0
|
||||
kick_start_time: 0.5
|
||||
|
||||
[temperature_sensor iHeater_S]
|
||||
sensor_pin: iHeater:TH1
|
||||
sensor_type: NTC 100K MGB18-104F39050L32
|
||||
min_temp: 0
|
||||
max_temp: 140
|
||||
|
||||
# [temperature_sensor iHeater_Chamber_ext]
|
||||
# sensor_pin: iHeater:TH2
|
||||
# sensor_type: NTC 100K MGB18-104F39050L32
|
||||
# min_temp: 0
|
||||
# max_temp: 140
|
||||
|
||||
[gcode_button mode_button]
|
||||
pin: iHeater:MODE
|
||||
# debounce_delay: 50
|
||||
press_gcode: iHEATER_OFF
|
||||
|
||||
# [output_pin led_1]
|
||||
# pin: iHeater:LED1
|
||||
|
||||
# [output_pin led_2]
|
||||
# pin: iHeater:LED2
|
||||
|
||||
# [output_pin led_3]
|
||||
# pin: iHeater:LED3
|
||||
|
||||
#######################################################
|
||||
##########################SETUP########################
|
||||
#######################################################
|
||||
|
||||
[gcode_macro CHAMBER_VARS]
|
||||
variable_chamber_target: 0 # target chamber temperature, °C
|
||||
variable_start_offset: 10 # printing can start when chamber reaches (target - offset)
|
||||
variable_delta_temp: 10 # delta between chamber and heater, °C
|
||||
variable_min_heater_temp: 50 # minimum heater temperature (for cooling), °C
|
||||
variable_max_heater_temp: 100 # maximum heater temperature, °C
|
||||
variable_control_interval: 1.0 # interval for control function call, seconds
|
||||
variable_air_min_delta: 0.5 # minimum difference between desired and current chamber temperature (heater temp = desired + delta_temp), °C
|
||||
variable_air_max_delta: 5.0 # maximum difference between desired and current chamber temperature (heater temp = max_heater_temp), °C
|
||||
gcode:
|
||||
|
||||
##########################MACRO########################
|
||||
|
||||
[gcode_macro M141]
|
||||
#rename_existing: M141.1
|
||||
gcode:
|
||||
# Get desired chamber temperature:
|
||||
# - if parameter S is passed, use it
|
||||
# - otherwise use chamber_target from CHAMBER_VARS
|
||||
{% set chamber_target = params.S|default(printer["gcode_macro CHAMBER_VARS"].chamber_target)|int %}
|
||||
# Get delta temperature:
|
||||
# - if parameter D is passed, use it
|
||||
# - otherwise use delta_temp from CHAMBER_VARS
|
||||
{% set delta_temp = params.D|default(printer["gcode_macro CHAMBER_VARS"].delta_temp)|int %}
|
||||
# Get maximum heater temperature:
|
||||
# - if parameter H is passed, use it
|
||||
# - otherwise use max_heater_temp from CHAMBER_VARS
|
||||
{% set max_heater_temp = params.H|default(printer["gcode_macro CHAMBER_VARS"].max_heater_temp)|int %}
|
||||
# Get interval for control function call
|
||||
{% set control_interval = printer["gcode_macro CHAMBER_VARS"].control_interval|float %}
|
||||
|
||||
RESPOND prefix="M141" msg="S{chamber_target} D{delta_temp} H{max_heater_temp}"
|
||||
|
||||
# Save parameters to CHAMBER_VARS for delayed_gcode
|
||||
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=chamber_target VALUE={chamber_target}
|
||||
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=delta_temp VALUE={delta_temp}
|
||||
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=max_heater_temp VALUE={max_heater_temp}
|
||||
|
||||
# Initial calculation
|
||||
iHEATER_CONTROL
|
||||
|
||||
# Start temperature control macro
|
||||
UPDATE_DELAYED_GCODE ID=_iHEATER_CONTROL DURATION={control_interval}
|
||||
|
||||
|
||||
[gcode_macro M191]
|
||||
#rename_existing: M191.1
|
||||
gcode:
|
||||
# Get desired chamber temperature:
|
||||
# - if parameter S is passed, use it
|
||||
# - otherwise use chamber_target from CHAMBER_VARS
|
||||
{% set chamber_target = params.S|default(printer["gcode_macro CHAMBER_VARS"].chamber_target)|int %}
|
||||
# Get delta temperature:
|
||||
# - if parameter D is passed, use it
|
||||
# - otherwise use delta_temp from CHAMBER_VARS
|
||||
{% set delta_temp = params.D|default(printer["gcode_macro CHAMBER_VARS"].delta_temp)|int %}
|
||||
# Get maximum heater temperature:
|
||||
# - if parameter H is passed, use it
|
||||
# - otherwise use max_heater_temp from CHAMBER_VARS
|
||||
{% set max_heater_temp = params.H|default(printer["gcode_macro CHAMBER_VARS"].max_heater_temp)|int %}
|
||||
# Get minimum chamber temperature for print start:
|
||||
# - if parameter W is passed, use it
|
||||
# - otherwise use start_offset from CHAMBER_VARS
|
||||
{% set start_offset = params.W|default(printer["gcode_macro CHAMBER_VARS"].start_offset)|int %}
|
||||
|
||||
RESPOND prefix="M191" msg="S{chamber_target} D{delta_temp} H{max_heater_temp} W{start_offset}"
|
||||
|
||||
M141 S{chamber_target} D{delta_temp} H{max_heater_temp}
|
||||
|
||||
# Save parameter to CHAMBER_VARS
|
||||
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=start_offset VALUE={start_offset}
|
||||
|
||||
# Wait until chamber reaches the target temperature
|
||||
TEMPERATURE_WAIT SENSOR="temperature_sensor iHeater_S" MINIMUM={chamber_target - start_offset}
|
||||
RESPOND prefix="M191" msg="Chamber reached {chamber_target - start_offset}°C"
|
||||
|
||||
|
||||
[gcode_macro iHEATER_ON]
|
||||
description: Alternative to M141 & M191 with alternative parameter names
|
||||
gcode:
|
||||
{% set s = params.CHAMBER_TEMP|default(printer["gcode_macro CHAMBER_VARS"].chamber_target)|int %}
|
||||
{% set d = params.DELTA|default(printer["gcode_macro CHAMBER_VARS"].delta_temp)|int %}
|
||||
{% set h = params.HEATER_MAX|default(printer["gcode_macro CHAMBER_VARS"].max_heater_temp)|int %}
|
||||
{% set w = params.CHAMBER_AWAIT_TEMP|default(printer["gcode_macro CHAMBER_VARS"].start_offset)|int %}
|
||||
|
||||
{% if w > 0 %}
|
||||
# If w is greater than 0, use M191 with wait for minimum temperature
|
||||
M191 S{s} D{d} H{h} W{s - w}
|
||||
{% else %}
|
||||
# Otherwise use M141 without waiting
|
||||
M141 S{s} D{d} H{h}
|
||||
{% endif %}
|
||||
|
||||
|
||||
[gcode_macro iHEATER_OFF]
|
||||
gcode:
|
||||
SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=chamber_target VALUE=0
|
||||
# SET_GCODE_VARIABLE MACRO=CHAMBER_VARS VARIABLE=start_offset VALUE=0
|
||||
|
||||
UPDATE_DELAYED_GCODE ID=_iHEATER_CONTROL DURATION=1
|
||||
RESPOND prefix="iHeater_off" msg="Heating disabled."
|
||||
|
||||
|
||||
[gcode_macro iHEATER_CONTROL]
|
||||
gcode:
|
||||
# Target chamber temperature
|
||||
{% set target_chamber_temp = printer["gcode_macro CHAMBER_VARS"].chamber_target|float %}
|
||||
# Delta - how much the heater temp exceeds chamber temp
|
||||
{% set delta_temp = printer["gcode_macro CHAMBER_VARS"].delta_temp|float %}
|
||||
# Minimum heater temperature
|
||||
{% set min_heater_temp = printer["gcode_macro CHAMBER_VARS"].min_heater_temp|float %}
|
||||
# Maximum heater temperature
|
||||
{% set max_heater_temp = printer["gcode_macro CHAMBER_VARS"].max_heater_temp|float %}
|
||||
# Current heater temperature
|
||||
{% set heater_temp = printer["heater_generic iHeater_H"].temperature|float %}
|
||||
# Current chamber air temperature (from sensor)
|
||||
{% set chamber_temp = printer["temperature_sensor iHeater_S"].temperature|float %}
|
||||
# Control function call interval
|
||||
{% set control_interval = printer["gcode_macro CHAMBER_VARS"].control_interval|float %}
|
||||
# Minimum difference between desired and current chamber temperature
|
||||
{% set air_min_delta = printer["gcode_macro CHAMBER_VARS"].air_min_delta|float %}
|
||||
# Maximum difference between desired and current chamber temperature
|
||||
{% set air_max_delta = printer["gcode_macro CHAMBER_VARS"].air_max_delta|float %}
|
||||
# Variable to set target heater temperature
|
||||
{% set target_heater_temp = 0.0 %}
|
||||
|
||||
# RESPOND prefix="iHeater_control" msg="Heater control..."
|
||||
|
||||
{% if target_chamber_temp > 0 %}
|
||||
# Heating is active
|
||||
{% set temp_diff = target_chamber_temp - chamber_temp %}
|
||||
|
||||
# MapToRange
|
||||
{% set value = temp_diff %}
|
||||
{% set inputLowerBound = air_min_delta %}
|
||||
{% set inputUpperBound = air_max_delta %}
|
||||
{% set outputLowerBound = delta_temp %}
|
||||
{% set outputUpperBound = [delta_temp, max_heater_temp - target_chamber_temp]|max %}
|
||||
|
||||
{% set slope = (outputUpperBound - outputLowerBound) / (inputUpperBound - inputLowerBound) %}
|
||||
{% set clamped = ([inputLowerBound, value, inputUpperBound]|sort)[1] %}
|
||||
{% set adjustment = outputLowerBound + slope * (clamped - inputLowerBound) %}
|
||||
{% set target_heater_temp = (target_chamber_temp + adjustment)|round(2) %}
|
||||
|
||||
{% elif heater_temp > min_heater_temp %}
|
||||
# Heating is off, but heater hasn't cooled down yet
|
||||
RESPOND prefix="iHeater_control" msg="Cooling down the heater..."
|
||||
{% else %}
|
||||
# Heater cooled down - end macro
|
||||
RESPOND prefix="iHeater_control" msg="Heater cooldown complete."
|
||||
{% set control_interval = 0.0 %}
|
||||
{% endif %}
|
||||
|
||||
SET_HEATER_TEMPERATURE HEATER=iHeater_H TARGET={target_heater_temp}
|
||||
UPDATE_DELAYED_GCODE ID=_iHEATER_CONTROL DURATION={control_interval}
|
||||
|
||||
|
||||
[delayed_gcode _iHEATER_CONTROL]
|
||||
gcode:
|
||||
iHEATER_CONTROL
|
||||
@@ -0,0 +1,26 @@
|
||||
#####################################################################
|
||||
# klipper-macros: include this one file from printer.cfg
|
||||
#
|
||||
# [include klipper-macros/klipper-macros.cfg]
|
||||
#
|
||||
# The order below matters: several files rename PAUSE/RESUME/CANCEL_PRINT
|
||||
# (rename_existing), and the last definition wins. It is the order these
|
||||
# files had on the original printer. Comment out what you do not use.
|
||||
#
|
||||
# Not part of this repo, include them yourself if you use them:
|
||||
# timelapse.cfg (moonraker-timelapse), TEST_SPEED.cfg (Ellis),
|
||||
# mainsail.cfg (before this file), blobifier, AFC, KAMP
|
||||
#####################################################################
|
||||
|
||||
[include bedfans.cfg]
|
||||
[include stealthburner_led_effects_barf.cfg]
|
||||
[include macros_klipperscreen.cfg]
|
||||
[include iHeater.cfg]
|
||||
[include drying.cfg]
|
||||
[include afc_macros.cfg]
|
||||
[include motion.cfg]
|
||||
[include heatsoak.cfg]
|
||||
[include purge_until_empty.cfg]
|
||||
[include purge_check.cfg]
|
||||
[include macros.cfg]
|
||||
[include bd_pa_afc_spoolman.cfg]
|
||||
@@ -0,0 +1,278 @@
|
||||
# Print start/end macros. Included by klipper-macros.cfg.
|
||||
|
||||
# [dynamicmacros]
|
||||
# configs: Macros/dynamicmacros.cfg
|
||||
|
||||
# [gcode_button kill_switch]
|
||||
# pin: gpio7
|
||||
# press_gcode: M112
|
||||
|
||||
[idle_timeout]
|
||||
gcode:
|
||||
TURN_OFF_HEATERS
|
||||
iHEATER_OFF ; Turns off the chamber heater
|
||||
M84
|
||||
# reset paused flag
|
||||
{% if printer.pause_resume.is_paused|lower == 'false' and park|lower == 'true'%}
|
||||
_TOOLHEAD_PARK_PAUSE_CANCEL
|
||||
{% endif %}
|
||||
_status_ready
|
||||
# A list of G-Code commands to execute on an idle timeout. See
|
||||
# docs/Command_Templates.md for G-Code format. The default is to run
|
||||
# "TURN_OFF_HEATERS" and "M84".
|
||||
timeout: 85400 # 24 hours
|
||||
# Idle time (in seconds) to wait before running the above G-Code
|
||||
# commands. The default is 600 seconds.
|
||||
|
||||
#####################################################################
|
||||
# Macros
|
||||
#####################################################################
|
||||
|
||||
# [gcode_macro PRINT_START]
|
||||
# gcode:
|
||||
# PRINT_WARMUP BED={params.BED} EXTRUDER={params.EXTRUDER} CHAMBER={params.CHAMBER|default("0")} TOOL={params.TOOL|default("0")}
|
||||
# PRINT_START_2 EXTRUDER={params.EXTRUDER}
|
||||
|
||||
[gcode_macro PRINT_WARMUP]
|
||||
description: Perform initial homing and heating tasks
|
||||
gcode:
|
||||
{% set target_bed = params.BED|int %}
|
||||
{% set target_extruder = params.EXTRUDER|int %}
|
||||
{% set target_chamber = params.CHAMBER|default("0")|int %}
|
||||
# {% set x_wait = printer.toolhead.axis_maximum.x|float / 2 %}
|
||||
# {% set y_wait = printer.toolhead.axis_maximum.y|float / 2 %}
|
||||
{% set initial_tool = params.TOOL|default("0")|int %}
|
||||
|
||||
{% set chamber_temp = printer["temperature_sensor chamber"].temperature|float %}
|
||||
|
||||
RESET_FAILURE
|
||||
CLEAR_PAUSE
|
||||
|
||||
_motion_detected
|
||||
|
||||
#SET_PIN PIN=caselight VALUE=1.00
|
||||
UPDATE_DELAYED_GCODE ID=_delayed_motors_off DURATION=0
|
||||
|
||||
BED_MESH_CLEAR
|
||||
SET_GCODE_OFFSET Z=0
|
||||
|
||||
CLEAR_PAUSE
|
||||
|
||||
SKEW_PROFILE LOAD=Califlower
|
||||
|
||||
{% set current_temp = printer.extruder.temperature %}
|
||||
{% if current_temp > 150 %}
|
||||
RESPOND MSG="nozzle to hot, lowering to 150"
|
||||
M104 S150 ; set nozzle temp to 150
|
||||
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
|
||||
{% endif %}
|
||||
|
||||
_status_homing
|
||||
RESPOND MSG="homing"
|
||||
HOME
|
||||
RESPOND MSG="homing finished"
|
||||
# G0 Z2 ; position beacon at 2mm for heat soak
|
||||
|
||||
{% if target_bed < 60 %}
|
||||
RESPOND MSG="Using PID for <60°C bed"
|
||||
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={target_bed} PID_KP=33.606 PID_KI=0.694 PID_KD=407.058
|
||||
{% else %}
|
||||
RESPOND MSG="Using PID for ≥60°C bed"
|
||||
SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={target_bed} pid_Kp=46.843 pid_Ki=2.002 pid_Kd=274.034
|
||||
{% endif %}
|
||||
|
||||
G1 X175 Y175 Z2 F1200
|
||||
_status_heating
|
||||
RESPOND MSG="heating"
|
||||
{% if target_bed > 0 %}
|
||||
M140 S{target_bed} ; start bed heater
|
||||
M104 S150 ; set nozzle temp to 150
|
||||
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
|
||||
M190 S{target_bed} ; wait on bed temperature
|
||||
{% endif %}
|
||||
|
||||
HOME
|
||||
QUAD_GANTRY_LEVEL
|
||||
BLOBIFIER_CLEAN ; call another macro to wipe nozzle if available
|
||||
G1 X175 Y175 Z5 F1200
|
||||
|
||||
T{initial_tool} #PURGE_LENGTH=100 #Load Initial Tool
|
||||
M104 S{target_extruder} ; set extruder to print temp
|
||||
M109 S{target_extruder} ; wait for extruder temp
|
||||
G10 ; retract
|
||||
|
||||
M104 S150 ; set nozzle temp to 150
|
||||
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
|
||||
|
||||
BLOBIFIER_CLEAN ; call another macro to wipe nozzle if available
|
||||
|
||||
G1 X175 Y175 Z5 F1200
|
||||
{% if target_chamber > 30 %}
|
||||
RESPOND MSG="waiting for chamber temperature"
|
||||
M141 S{target_chamber}
|
||||
# M191 S{target_chamber} ; Waits until the chamber temperature reaches 60°C
|
||||
# TEMPERATURE_WAIT SENSOR="temperature_sensor chamber" MINIMUM={target_chamber}
|
||||
|
||||
M106 S255
|
||||
# HEAT_SOAK HEATER='heater_bed' TARGET={BED_TEMP} SOAKER='temperature_sensor iHeater_S' SOAK_TEMP={target_chamber} TIMEOUT=60 CANCEL='_NOTHING'
|
||||
HEAT_SOAK HEATER='heater_bed' TARGET={target_bed} SOAKER='temperature_sensor chamber' SOAK_TEMP={target_chamber} TIMEOUT=60 CANCEL='_NOTHING'
|
||||
# (CANCEL='_NOTHING') and having unreachable SOAK_TEMP makes it run till the TIMEOUT and continue without canceling the print.
|
||||
{% else %}
|
||||
RESPOND MSG="waiting for bed core temperature"
|
||||
|
||||
HEAT_SOAK HEATER='heater_bed' TARGET={target_bed} SOAKER='temperature_sensor bed_core' SOAK_TEMP={target_bed - 5} TIMEOUT=30 CANCEL='_NOTHING'
|
||||
# (CANCEL='_NOTHING') and having unreachable SOAK_TEMP makes it run till the TIMEOUT and continue without canceling the print.
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro _NOTHING]
|
||||
gcode:
|
||||
_HEAT_SOAK__BASE_RESUME
|
||||
|
||||
[gcode_macro PRINT_START]
|
||||
gcode:
|
||||
RESPOND MSG="print start macro continued"
|
||||
|
||||
_motion_detected
|
||||
|
||||
{% set target_extruder = params.EXTRUDER|int %}
|
||||
|
||||
M106 S0
|
||||
|
||||
M104 S150 ; set nozzle temp to 150
|
||||
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
|
||||
|
||||
HOME
|
||||
BLOBIFIER_CLEAN ; call another macro to wipe nozzle if available
|
||||
|
||||
_status_calibrating_z
|
||||
G28 Z METHOD=CONTACT CALIBRATE=1 ; calibrate z offset and beacon model hot
|
||||
|
||||
_status_leveling
|
||||
RESPOND MSG="quad ganrty level"
|
||||
QUAD_GANTRY_LEVEL
|
||||
|
||||
_status_meshing
|
||||
RESPOND MSG="meshing"
|
||||
BED_MESH_CALIBRATE RUNS=2 ; bed mesh in scan mode
|
||||
|
||||
BLOBIFIER_CLEAN ; call another macro to wipe nozzle if available
|
||||
|
||||
M104 S150 ; set nozzle temp to 150
|
||||
M109 S150 ; wait until nozzle reaches 150 ; wait until nozzle reaches 0
|
||||
|
||||
_status_calibrating_z
|
||||
G28 Z METHOD=CONTACT CALIBRATE=1 ; calibrate z offset and beacon model hot
|
||||
|
||||
_status_heating
|
||||
M104 S{target_extruder} ; set extruder to print temp
|
||||
M109 S{target_extruder} ; wait for extruder temp
|
||||
|
||||
# ; needs fine tuning, long meltzones require more
|
||||
# SET_GCODE_OFFSET Z_ADJUST={OFFSET} ; apply optional material squish via slicer
|
||||
#SET_GCODE_OFFSET Z=-0.06 ; squish
|
||||
# SET_GCODE_OFFSET Z=0.02 ; squish # worked before but a bit to high now
|
||||
# SET_GCODE_OFFSET Z=-0.05 ; squish
|
||||
SET_GCODE_OFFSET Z=-0.00 ; squish
|
||||
|
||||
RESPOND MSG="starting"
|
||||
;PRIME_NOZZLE ; call another macro to purge or prime nozzle
|
||||
# WIPE_NOZZLE ; call another macro to wipe nozzle if available
|
||||
# Line_Purge
|
||||
# AFC_POOP PURGE_LENGTH=100
|
||||
# AFC_KICK
|
||||
|
||||
BLOBIFIER_CLEAN
|
||||
|
||||
PA_CALIBRATE_ALL_LANES LANES="lane1,lane2,lane3,lane4,lane5"
|
||||
|
||||
M104 S{target_extruder} ; set extruder to print temp
|
||||
M109 S{target_extruder} ; wait for extruder temp
|
||||
|
||||
BLOBIFIER_CLEAN
|
||||
|
||||
; start printing
|
||||
|
||||
G90 ; absolute positioning
|
||||
G1 Z20 F30000 ; move nozzle away from bed
|
||||
G1 X175 Y175 F30000 ; move nozzle away from bed
|
||||
|
||||
G28 XYZ
|
||||
|
||||
#G11 ; unretract
|
||||
|
||||
_AFC_STORE_HOME_REFERENCE
|
||||
|
||||
SET_BDWIDTH NAME=fila_width_0 COMMAND=ENABLE_ALL
|
||||
# SET_BDWIDTH NAME=fila_width_0 COMMAND=ENABLE_MOTION
|
||||
|
||||
_status_printing
|
||||
|
||||
|
||||
[gcode_macro PRINT_END]
|
||||
# Use PRINT_END for the slicer ending script - please customise for your slicer of choice
|
||||
gcode:
|
||||
# safe anti-stringing move coords
|
||||
{% set th = printer.toolhead %}
|
||||
{% set x_safe = th.position.x + 20 * (1 if th.axis_maximum.x - th.position.x > 20 else -1) %}
|
||||
{% set y_safe = th.position.y + 20 * (1 if th.axis_maximum.y - th.position.y > 20 else -1) %}
|
||||
{% set z_safe = [th.position.z + 2, th.axis_maximum.z]|min %}
|
||||
|
||||
SAVE_GCODE_STATE NAME=STATE_PRINT_END
|
||||
|
||||
_motion_detected
|
||||
|
||||
M221 S100
|
||||
|
||||
M400 ; wait for buffer to clear
|
||||
G92 E0 ; zero the extruder
|
||||
# G1 E-0.5 F1800 ; retract filament
|
||||
G10 ; retract
|
||||
|
||||
TURN_OFF_HEATERS
|
||||
iHEATER_OFF ; Turns off the chamber heater
|
||||
|
||||
G90 ; absolute positioning
|
||||
G0 X{x_safe} Y{y_safe} Z{z_safe} F20000 ; move nozzle to remove stringing
|
||||
G0 X{th.axis_maximum.x//2} Y{th.axis_maximum.y - 5} F3600 ; park nozzle at rear
|
||||
M107 ; turn off fan
|
||||
|
||||
{% if printer.pause_resume.is_paused|lower == 'false' and park|lower == 'true'%}
|
||||
_TOOLHEAD_PARK_PAUSE_CANCEL
|
||||
{% endif %}
|
||||
|
||||
SET_BDWIDTH NAME=fila_width_0 COMMAND=DISABLE ;disable the bdwidth
|
||||
|
||||
#SET_PIN PIN=caselight VALUE=0.03
|
||||
_status_part_ready
|
||||
|
||||
UPDATE_DELAYED_GCODE ID=_delayed_motors_off DURATION=3600
|
||||
|
||||
BED_MESH_CLEAR
|
||||
SET_SKEW CLEAR=1
|
||||
RESTORE_GCODE_STATE NAME=STATE_PRINT_END
|
||||
|
||||
[delayed_gcode _delayed_motors_off]
|
||||
gcode:
|
||||
M84
|
||||
|
||||
# [gcode_macro QUAD_GANTRY_LEVEL]
|
||||
# rename_existing: _QUAD_GANTRY_LEVEL
|
||||
# gcode:
|
||||
# _QUAD_GANTRY_LEVEL
|
||||
# _QUAD_GANTRY_LEVEL PROBE_METHOD=contact
|
||||
|
||||
### bed warp visualizer
|
||||
[gcode_macro SOAK_MY_BED]
|
||||
description: Heat soak bed and live meassure
|
||||
rename_existing: SOAK_MY_BED_BASE
|
||||
gcode:
|
||||
{% set temp = params.TEMPERATURE|default(60) %}
|
||||
{% set duration = params.DURATION|default(10) %}
|
||||
{% set heater = params.HEATER|default("heater_bed") %}
|
||||
{% set mesh = params.MESH_COMMAND|default("BED_MESH_CALIBRATE METHOD=beacon") %}
|
||||
|
||||
G28
|
||||
QUAD_GANTRY_LEVEL
|
||||
|
||||
SOAK_MY_BED_BASE TEMPERATURE={temp} DURATION={duration} HEATER={heater} MESH_COMMAND="{mesh}"
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# https://klipperscreen.readthedocs.io/en/latest/macros/#recommended
|
||||
[gcode_macro PAUSE]
|
||||
description: Pause the actual running print
|
||||
rename_existing: PAUSE_BASE
|
||||
variable_saved_fan_speed: 0.0
|
||||
gcode:
|
||||
# SAVE_GCODE_STATE NAME=PAUSE_MACRO_state
|
||||
# Save current fan speed
|
||||
{% set current_fan = printer.fan.speed %}
|
||||
SET_GCODE_VARIABLE MACRO=PAUSE VARIABLE=saved_fan_speed VALUE={current_fan}
|
||||
|
||||
# Current position
|
||||
{% set x = printer.toolhead.position.x|float %}
|
||||
{% set y = printer.toolhead.position.y|float %}
|
||||
|
||||
# Determine target positions
|
||||
{% set target_x = 340 if (x < 340 or x > 350) else x %}
|
||||
{% set target_y = 340 if (y < 340 or y > 350) else y %}
|
||||
|
||||
# Only move if needed
|
||||
{% if target_x != x or target_y != y %}
|
||||
G90
|
||||
G1 X{target_x} Y{target_y} F12000
|
||||
{% endif %}
|
||||
|
||||
# Pause print
|
||||
PAUSE_BASE
|
||||
|
||||
_TOOLHEAD_PARK_PAUSE_CANCEL
|
||||
# Set fan to S28 (28/255 ≈ 0.1098)
|
||||
M106 S28
|
||||
|
||||
[gcode_macro RESUME]
|
||||
description: Resume the actual running print
|
||||
rename_existing: RESUME_BASE
|
||||
variable_skip_restore: 0
|
||||
gcode:
|
||||
{% set extrude = printer['gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL'].extrude %}
|
||||
{% set skip = printer['gcode_macro RESUME'].skip_restore|int %}
|
||||
#### get VELOCITY parameter if specified ####
|
||||
{% if 'VELOCITY' in params|upper %}
|
||||
{% set get_params = ('VELOCITY=' + params.VELOCITY) %}
|
||||
{%else %}
|
||||
{% set get_params = "" %}
|
||||
{% endif %}
|
||||
##### end of definitions #####
|
||||
{% if printer.extruder.can_extrude|lower == 'true' %}
|
||||
M83
|
||||
G1 E{extrude} F2100
|
||||
{% if printer.gcode_move.absolute_extrude |lower == 'true' %} M82 {% endif %}
|
||||
{% else %}
|
||||
{action_respond_info("Extruder not hot enough")}
|
||||
{% endif %}
|
||||
|
||||
#BLOBIFIER_CLEAN
|
||||
#G1 Z10
|
||||
|
||||
# Restore saved fan speed
|
||||
{% set old_fan = printer["gcode_macro PAUSE"].saved_fan_speed %}
|
||||
{% if old_fan > 0 %}
|
||||
{% set fan_pwm = (old_fan * 255) | int %}
|
||||
{% if fan_pwm > 255 %}
|
||||
{% set fan_pwm = 255 %}
|
||||
{% endif %}
|
||||
M106 S{fan_pwm}
|
||||
{% else %}
|
||||
M107
|
||||
{% endif %}
|
||||
|
||||
{% if skip == 0 %}
|
||||
# RESTORE_GCODE_STATE name=PAUSE_MACRO_state
|
||||
RESUME_BASE {get_params}
|
||||
{% else %}
|
||||
RESPOND PREFIX="AFC" MSG="Skipping RESTORE_GCODE_STATE (drift resume)"
|
||||
{% endif %}
|
||||
|
||||
_status_printing
|
||||
CLEAR_PAUSE
|
||||
RESET_FAILURE
|
||||
SET_GCODE_VARIABLE MACRO=RESUME VARIABLE=skip_restore VALUE=0
|
||||
|
||||
[gcode_macro _TOOLHEAD_PARK_PAUSE_CANCEL]
|
||||
description: Helper: park toolhead used in PAUSE and CANCEL_PRINT
|
||||
variable_extrude: 2.0
|
||||
gcode:
|
||||
##### set park positon for x and y #####
|
||||
# default is your max posion from your printer.cfg
|
||||
|
||||
# {% set x_park = printer.toolhead.axis_maximum.x|float - 5.0 %}
|
||||
{% set x_park = 20.0 %}
|
||||
{% set y_park = printer.toolhead.axis_maximum.y|float - 5.0 %}
|
||||
{% set z_park_delta = 2.0 %}
|
||||
##### calculate save lift position #####
|
||||
{% set max_z = printer.toolhead.axis_maximum.z|float %}
|
||||
{% set act_z = printer.toolhead.position.z|float %}
|
||||
{% if act_z < (max_z - z_park_delta) %}
|
||||
{% set z_safe = z_park_delta %}
|
||||
{% else %}
|
||||
{% set z_safe = max_z - act_z %}
|
||||
{% endif %}
|
||||
##### end of definitions #####
|
||||
{% if printer.extruder.can_extrude|lower == 'true' %}
|
||||
M83
|
||||
G1 E-{extrude} F2100
|
||||
{% if printer.gcode_move.absolute_extrude |lower == 'true' %} M82 {% endif %}
|
||||
{% else %}
|
||||
{action_respond_info("Extruder not hot enough")}
|
||||
{% endif %}
|
||||
{% if "xyz" in printer.toolhead.homed_axes %}
|
||||
G91
|
||||
G1 Z{z_safe} F900
|
||||
G90
|
||||
G1 X{x_park} Y{y_park} F6000
|
||||
{% if printer.gcode_move.absolute_coordinates|lower == 'false' %} G91 {% endif %}
|
||||
{% else %}
|
||||
{action_respond_info("Printer not homed")}
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro CANCEL_PRINT]
|
||||
description: Cancel the actual running print
|
||||
rename_existing: CANCEL_PRINT_BASE_KLIPPERSCREEN
|
||||
variable_park: True
|
||||
gcode:
|
||||
## Move head and retract only if not already in the pause state and park set to true
|
||||
|
||||
{% if printer.pause_resume.is_paused|lower == 'false' and park|lower == 'true'%}
|
||||
_TOOLHEAD_PARK_PAUSE_CANCEL
|
||||
{% endif %}
|
||||
|
||||
STOP_HEAT_SOAK
|
||||
|
||||
TURN_OFF_HEATERS
|
||||
CANCEL_PRINT_BASE_KLIPPERSCREEN
|
||||
|
||||
[gcode_macro LOAD_FILAMENT]
|
||||
variable_load_distance: 60
|
||||
variable_purge_distance: 20
|
||||
gcode:
|
||||
{% set speed = params.SPEED|default(15) %}
|
||||
{% set max_velocity = printer.configfile.settings['extruder'].max_extrude_only_velocity * 15 %}
|
||||
SAVE_GCODE_STATE NAME=load_state
|
||||
G91
|
||||
G92 E0
|
||||
G1 E{load_distance} F{max_velocity} # fast-load
|
||||
G1 E{purge_distance} F{speed} # purge
|
||||
RESTORE_GCODE_STATE NAME=load_state
|
||||
|
||||
[gcode_macro UNLOAD_FILAMENT]
|
||||
variable_unload_distance: 100
|
||||
variable_purge_distance: 0
|
||||
gcode:
|
||||
{% set speed = params.SPEED|default(15) %}
|
||||
{% set max_velocity = printer.configfile.settings['extruder'].max_extrude_only_velocity * 30 %}
|
||||
SAVE_GCODE_STATE NAME=unload_state
|
||||
G91
|
||||
G92 E0
|
||||
G1 E{purge_distance} F{speed} # purge
|
||||
G1 E-20 F100# unload
|
||||
G1 E-{unload_distance} F{max_velocity} # fast-unload
|
||||
RESTORE_GCODE_STATE NAME=unload_state
|
||||
|
||||
|
||||
|
||||
[gcode_macro M600]
|
||||
#
|
||||
# M600
|
||||
# https://github.com/simplisticton/common-macros/tree/main
|
||||
#
|
||||
# Mimic Marlin's Pause for filament change command
|
||||
description: Pause for filament change
|
||||
gcode:
|
||||
PAUSE
|
||||
M117 Paused
|
||||
M118 Print paused for filament change... type "Resume" to continue
|
||||
@@ -0,0 +1,40 @@
|
||||
[gcode_button am312]
|
||||
pin: !PC0
|
||||
press_gcode:
|
||||
_motion_detected
|
||||
|
||||
[gcode_macro _motion_detected]
|
||||
description: Motion sensor triggered
|
||||
gcode:
|
||||
{% set printing = printer.print_stats.state == "printing" %}
|
||||
|
||||
# RESPOND MSG="AM312: Motion detected"
|
||||
|
||||
{% if printing %}
|
||||
# RESPOND MSG="AM312: Printer is printing -> light ON (100%)"
|
||||
SET_PIN PIN=caselight VALUE=1.0
|
||||
_set_nozzle_leds_on
|
||||
{% else %}
|
||||
# RESPOND MSG="AM312: Printer idle -> light ON (10%)"
|
||||
SET_PIN PIN=caselight VALUE=0.1
|
||||
_set_nozzle_leds_off
|
||||
{% endif %}
|
||||
|
||||
# RESPOND MSG="AM312: Resetting timeout to 30 seconds"
|
||||
UPDATE_DELAYED_GCODE ID=_motion_timeout DURATION=30
|
||||
|
||||
|
||||
[delayed_gcode _motion_timeout]
|
||||
gcode:
|
||||
{% set printing = printer.print_stats.state == "printing" %}
|
||||
|
||||
# RESPOND MSG="AM312: Timeout reached, no motion"
|
||||
|
||||
{% if printing %}
|
||||
# RESPOND MSG="AM312: Printing -> dim light to 10%"
|
||||
SET_PIN PIN=caselight VALUE=0.1
|
||||
{% else %}
|
||||
# RESPOND MSG="AM312: Idle -> light OFF"
|
||||
SET_PIN PIN=caselight VALUE=0.0
|
||||
{% endif %}
|
||||
_set_nozzle_leds_off
|
||||
@@ -0,0 +1,155 @@
|
||||
#####################################################################
|
||||
# Purge check - did the filament really reach the nozzle during the
|
||||
# Blobifier purge after a tool change?
|
||||
#
|
||||
# Two independent checks around the purge AFC runs after every load:
|
||||
# 1. bdwidth motion: extruded mm vs mm the sensor saw moving past it.
|
||||
# Catches a grinding extruder, a tangle or a snapped filament.
|
||||
# 2. bd_pressure (needs the load-check firmware extension): how long
|
||||
# the nozzle pressure stayed above the threshold. Catches filament
|
||||
# that never reached the melt zone. Skipped automatically on stock
|
||||
# firmware or when bdpressure_check.py is not installed.
|
||||
#
|
||||
# Needs: bdwidth_status.py in ~/klipper/klippy/extras/
|
||||
# bdpressure_check.py in ~/klipper/klippy/extras/ (optional)
|
||||
# (klipper/install.sh links both; guide: klipper/load_check/afc_blobifier/README.md)
|
||||
# Hook: in [AFC] (AFC/AFC.cfg) change poop_cmd: BLOBIFIER
|
||||
# to poop_cmd: BLOBIFIER_CHECKED
|
||||
#
|
||||
# Rollout:
|
||||
# Phase 1 mode "report": only prints the results after every purge.
|
||||
# Collect a few days of values, then set min_ratio and
|
||||
# pressure_min_ms well below the lowest good ones.
|
||||
# Phase 2 mode "enforce": pauses when a check fails.
|
||||
#
|
||||
# Costs about 2 x (sample_time + 0.5 s) per swap, 5 s at sample_time 2,
|
||||
# so the sensors are quiet and up to date before and after the purge.
|
||||
# When bdwidth is not reading, add bdwidth_warmup_ms: it is switched on for
|
||||
# the check (motion only, SET_BDWIDTH ENABLE_MOTION) with its runout pause
|
||||
# off meanwhile (SET_FILAMENT_SENSOR), and outside a print off again after.
|
||||
#####################################################################
|
||||
|
||||
[bdwidth_status]
|
||||
sensor: fila_width_0
|
||||
|
||||
# Remove this section if bdpressure_check.py is not installed
|
||||
[bdpressure_check]
|
||||
sensor: bd_pa
|
||||
|
||||
[gcode_macro _PURGE_CHECK_CFG]
|
||||
variable_mode: "enforce" # report | enforce | off
|
||||
variable_motion_check: False # False: skip the bdwidth motion check, pressure only
|
||||
variable_min_ratio: 0.4 # placeholder: sensed mm / extruded mm below this = not feeding
|
||||
variable_min_extrude: 20 # skip the checks for purges shorter than this (mm)
|
||||
variable_pressure_min_ms: 5000 # placeholder: longest continuous pressure needed (ms)
|
||||
# bdwidth is not reading after SET_BDWIDTH ... COMMAND=DISABLE and after
|
||||
# every Klipper restart until an ENABLE (outside a print, often early in a
|
||||
# print start). The check then switches it on and waits this long for the
|
||||
# first readings (its first read often fails and it retries after 10 s).
|
||||
variable_bdwidth_warmup_ms: 15000
|
||||
variable_start_counts: 0
|
||||
variable_start_e: 0.0
|
||||
gcode:
|
||||
|
||||
[gcode_macro BLOBIFIER_CHECKED]
|
||||
description: Blobifier purge with bdwidth motion and bd_pressure checks (use as poop_cmd in [AFC])
|
||||
gcode:
|
||||
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
|
||||
{% set wait = (printer.bdwidth_status.sample_time * 1000 + 500)|int %}
|
||||
{% set pressure = 'bdpressure_check' in printer %}
|
||||
{% set bdw = printer.bdwidth_status.name %}
|
||||
# If bdwidth is not reading (outside a print, early in a print start,
|
||||
# after a Klipper restart) it is switched on for the check, with its
|
||||
# runout response off meanwhile, so an empty lane does not make bdwidth
|
||||
# pause the printer (the check reports it instead). Afterwards the
|
||||
# runout response is restored; bdwidth itself is only switched off
|
||||
# again outside a print, so a print never loses it.
|
||||
{% set bdw_on = cfg.motion_check and not printer.bdwidth_status.reading|default(false) %}
|
||||
{% set bdw_off = bdw_on and printer.print_stats.state not in ["printing", "paused"] %}
|
||||
{% set runout_was = 1 if printer.bdwidth_status.enabled else 0 %}
|
||||
# Without the motion check, bdwidth's own runout response is off during
|
||||
# the purge too, so a bdwidth that does not see the movement cannot
|
||||
# pause the printer here; the pressure check judges the purge.
|
||||
{% set runout_off = bdw_on or not cfg.motion_check %}
|
||||
{% if cfg.mode == "off" %}
|
||||
BLOBIFIER {rawparams}
|
||||
{% else %}
|
||||
M400
|
||||
{% if runout_off %}
|
||||
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE=0
|
||||
{% endif %}
|
||||
{% if bdw_on %}
|
||||
SET_BDWIDTH NAME={bdw} COMMAND=ENABLE_MOTION
|
||||
G4 P{cfg.bdwidth_warmup_ms}
|
||||
{% endif %}
|
||||
G4 P{wait}
|
||||
_PURGE_CHECK_START
|
||||
{% if pressure %}
|
||||
BDP_CHECK_START
|
||||
{% endif %}
|
||||
BLOBIFIER {rawparams}
|
||||
M400
|
||||
{% if pressure %}
|
||||
BDP_CHECK_STOP
|
||||
{% endif %}
|
||||
G4 P{wait}
|
||||
_PURGE_CHECK_END
|
||||
{% if bdw_off %}
|
||||
SET_BDWIDTH NAME={bdw} COMMAND=DISABLE
|
||||
{% endif %}
|
||||
{% if runout_off %}
|
||||
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE={runout_was}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
# Separate macros so the status is read when they run, not when
|
||||
# BLOBIFIER_CHECKED is rendered
|
||||
[gcode_macro _PURGE_CHECK_START]
|
||||
gcode:
|
||||
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=start_counts VALUE={printer.bdwidth_status.total_move}
|
||||
SET_GCODE_VARIABLE MACRO=_PURGE_CHECK_CFG VARIABLE=start_e VALUE={printer.toolhead.position.e}
|
||||
|
||||
[gcode_macro _PURGE_CHECK_END]
|
||||
gcode:
|
||||
{% set cfg = printer["gcode_macro _PURGE_CHECK_CFG"] %}
|
||||
{% set s = printer.bdwidth_status %}
|
||||
{% set lane = printer.AFC.current_load|default("?", true) %}
|
||||
{% set extruded = printer.toolhead.position.e - cfg.start_e %}
|
||||
{% set failed = [] %}
|
||||
{% if extruded < cfg.min_extrude %}
|
||||
RESPOND MSG="PURGE_CHECK {lane}: only {'%.1f' % extruded} mm extruded, skipped"
|
||||
{% else %}
|
||||
{% set motion_on = cfg.motion_check and s.active != "disable" %}
|
||||
{% if not cfg.motion_check %}
|
||||
{% set motion = "motion check off" %}
|
||||
{% elif not motion_on %}
|
||||
{% set motion = "motion skipped (bdwidth disabled)" %}
|
||||
{% else %}
|
||||
{% set sensed = (s.total_move - cfg.start_counts)|abs / s.linear_motion %}
|
||||
{% set ratio = sensed / extruded %}
|
||||
{% set motion = "motion %.1f/%.1f mm ratio %.2f, width %.2f" % (sensed, extruded, ratio, s.diameter) %}
|
||||
{% if ratio < cfg.min_ratio %}
|
||||
{% if s.total_move == 0 and s.diameter == 0 %}
|
||||
{% set _ = failed.append("no data from bdwidth since Klipper start (sensor connected?)") %}
|
||||
{% else %}
|
||||
{% set _ = failed.append("filament not moving past bdwidth") %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if 'bdpressure_check' in printer and printer.bdpressure_check.available %}
|
||||
{% set p = printer.bdpressure_check %}
|
||||
{% set press = "pressure longest %d ms, total %d ms, peak %d" % (p.longest_ms, p.above_ms, p.peak) %}
|
||||
{% if p.longest_ms < cfg.pressure_min_ms %}
|
||||
{% set _ = failed.append("no sustained nozzle pressure") %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% set press = "pressure skipped (no check firmware)" %}
|
||||
{% endif %}
|
||||
{% set checked = motion_on or 'bdpressure_check' in printer and printer.bdpressure_check.available %}
|
||||
{% set verdict = 'FAIL: ' ~ failed|join(', ') if failed else ('OK' if checked else 'NOTHING CHECKED') %}
|
||||
RESPOND MSG="PURGE_CHECK {lane}: {motion}; {press} -> {verdict}"
|
||||
{% if failed and cfg.mode == "enforce" %}
|
||||
RESPOND TYPE=error MSG="PURGE_CHECK: {lane} {failed|join(', ')} - check extruder and lane"
|
||||
PAUSE
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,127 @@
|
||||
#####################################################################
|
||||
# PURGE_UNTIL_EMPTY - purge a broken filament out through the nozzle
|
||||
#
|
||||
# When the filament snaps inside the PTFE tube, the piece below the break
|
||||
# is still in the extruder. This macro repeats the Blobifier purge and
|
||||
# measures each one with bd_pressure (load-check firmware). It stops after
|
||||
# the first purge without sustained nozzle pressure: the extruder no longer
|
||||
# has filament to push.
|
||||
#
|
||||
# What stays behind: the extruder can only push filament its gears still
|
||||
# grip. Once the tail has passed the gears, the last bit between the gears
|
||||
# and the nozzle stays in the hotend. The next normal load pushes it out.
|
||||
#
|
||||
# The AFC lane is unsynced first (UNSET_LANE_LOADED). Otherwise AFC keeps
|
||||
# feeding the lane together with the extruder, the spool side of the break
|
||||
# can catch up and the purge never ends. Afterwards AFC sees no lane loaded:
|
||||
# eject or reload the lane as usual.
|
||||
#
|
||||
# bdwidth sits at the start of the tube, so after the break it sees no
|
||||
# filament while the extruder keeps pushing. Its runout response (motion or
|
||||
# width) would pause the printer mid-purge, so it is switched off during the
|
||||
# purges (SET_FILAMENT_SENSOR) and restored afterwards. bdwidth is not used
|
||||
# to decide when to stop: only the nozzle pressure can see the last piece.
|
||||
#
|
||||
# Needs: bdpressure_check.py + [bdpressure_check] (see purge_check.cfg)
|
||||
# Blobifier, homed printer, hot nozzle (the macro heats if needed)
|
||||
#
|
||||
# Usage (printer idle or paused, not while printing):
|
||||
# PURGE_UNTIL_EMPTY defaults below
|
||||
# PURGE_UNTIL_EMPTY TEMP=240 MAX=10 filament temperature, purge limit
|
||||
# PURGE_UNTIL_EMPTY LENGTH=100 mm per purge (default: Blobifier's)
|
||||
# PURGE_UNTIL_EMPTY UNSYNC=0 keep the AFC lane synced
|
||||
#
|
||||
# Each purge prints one line; the end result says whether the nozzle ran
|
||||
# empty or MAX purges were reached (lane still feeding, or nozzle blocked).
|
||||
#####################################################################
|
||||
|
||||
[gcode_macro PURGE_UNTIL_EMPTY]
|
||||
description: Repeat the Blobifier purge until bd_pressure measures no more filament
|
||||
variable_max_purges: 15 # safety limit: ~1.2 m of bowden is ~8 purges of 150 mm
|
||||
variable_empty_ms: 5000 # longest pressure below this = no filament (good purge ~17 s, empty ~30 ms)
|
||||
variable_done: 0
|
||||
variable_count: 0
|
||||
gcode:
|
||||
{% set cfg = printer["gcode_macro PURGE_UNTIL_EMPTY"] %}
|
||||
{% set max_purges = params.MAX|default(cfg.max_purges)|int %}
|
||||
{% set length = params.LENGTH|default(0)|float %}
|
||||
{% set unsync = params.UNSYNC|default(1)|int %}
|
||||
{% set min_temp = printer.configfile.settings.extruder.min_extrude_temp|float %}
|
||||
{% set temp = params.TEMP|default([printer.extruder.target, 0]|max)|float %}
|
||||
{% if 'bdpressure_check' not in printer %}
|
||||
{action_raise_error("PURGE_UNTIL_EMPTY: needs bdpressure_check.py and [bdpressure_check]")}
|
||||
{% elif printer.print_stats.state == "printing" %}
|
||||
{action_raise_error("PURGE_UNTIL_EMPTY: printer is printing, pause the print first")}
|
||||
{% elif "xyz" not in printer.toolhead.homed_axes %}
|
||||
{action_raise_error("PURGE_UNTIL_EMPTY: home the printer first")}
|
||||
{% elif temp < min_temp %}
|
||||
{action_raise_error("PURGE_UNTIL_EMPTY: give the filament temperature, e.g. PURGE_UNTIL_EMPTY TEMP=240")}
|
||||
{% else %}
|
||||
BDP_CHECK_QUERY
|
||||
_PURGE_UNTIL_EMPTY_RUN MAX={max_purges} LENGTH={length} UNSYNC={unsync} TEMP={temp}
|
||||
{% endif %}
|
||||
|
||||
# Rendered after BDP_CHECK_QUERY, so 'available' is current.
|
||||
[gcode_macro _PURGE_UNTIL_EMPTY_RUN]
|
||||
gcode:
|
||||
{% if not printer.bdpressure_check.available %}
|
||||
{action_raise_error("PURGE_UNTIL_EMPTY: bd_pressure load-check firmware not found")}
|
||||
{% endif %}
|
||||
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=done VALUE=0
|
||||
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE=0
|
||||
{% if params.UNSYNC|int and printer.AFC.current_load %}
|
||||
RESPOND MSG="PURGE_UNTIL_EMPTY: unsyncing {printer.AFC.current_load} from the extruder (UNSET_LANE_LOADED)"
|
||||
UNSET_LANE_LOADED
|
||||
{% endif %}
|
||||
{% set bdw = printer.bdwidth_status.name if 'bdwidth_status' in printer else '' %}
|
||||
{% set runout_was = 1 if bdw and printer.bdwidth_status.enabled else 0 %}
|
||||
{% if runout_was %}
|
||||
RESPOND MSG="PURGE_UNTIL_EMPTY: bdwidth runout response off during the purges"
|
||||
SET_FILAMENT_SENSOR SENSOR={bdw} ENABLE=0
|
||||
{% endif %}
|
||||
M109 S{params.TEMP}
|
||||
{% for i in range(params.MAX|int) %}
|
||||
_PURGE_UNTIL_EMPTY_STEP LENGTH={params.LENGTH}
|
||||
{% endfor %}
|
||||
_PURGE_UNTIL_EMPTY_RESULT MAX={params.MAX} BDW={bdw if runout_was else '-'}
|
||||
|
||||
# Rendered when it runs, so it sees the result of the previous purge.
|
||||
[gcode_macro _PURGE_UNTIL_EMPTY_STEP]
|
||||
gcode:
|
||||
{% if not printer["gcode_macro PURGE_UNTIL_EMPTY"].done %}
|
||||
{% set length = params.LENGTH|default(0)|float %}
|
||||
M400
|
||||
G4 P500
|
||||
BDP_CHECK_START
|
||||
BLOBIFIER {'PURGE_LENGTH=%.1f' % length if length > 0 else ''}
|
||||
M400
|
||||
BDP_CHECK_STOP
|
||||
_PURGE_UNTIL_EMPTY_EVAL
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro _PURGE_UNTIL_EMPTY_EVAL]
|
||||
gcode:
|
||||
{% set cfg = printer["gcode_macro PURGE_UNTIL_EMPTY"] %}
|
||||
{% set p = printer.bdpressure_check %}
|
||||
{% set n = cfg.count + 1 %}
|
||||
{% if not p.available %}
|
||||
{action_raise_error("PURGE_UNTIL_EMPTY: lost the bd_pressure load-check reading, stopped. bdwidth runout response may still be off: SET_FILAMENT_SENSOR SENSOR=<bdwidth name> ENABLE=1")}
|
||||
{% endif %}
|
||||
{% set empty = p.longest_ms < cfg.empty_ms %}
|
||||
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=count VALUE={n}
|
||||
RESPOND MSG="PURGE_UNTIL_EMPTY purge {n}: pressure longest {p.longest_ms} ms, total {p.above_ms} ms, peak {p.peak} -> {'empty' if empty else 'filament'}"
|
||||
{% if empty %}
|
||||
SET_GCODE_VARIABLE MACRO=PURGE_UNTIL_EMPTY VARIABLE=done VALUE=1
|
||||
{% endif %}
|
||||
|
||||
[gcode_macro _PURGE_UNTIL_EMPTY_RESULT]
|
||||
gcode:
|
||||
{% set cfg = printer["gcode_macro PURGE_UNTIL_EMPTY"] %}
|
||||
{% if params.BDW|default('-') != '-' %}
|
||||
SET_FILAMENT_SENSOR SENSOR={params.BDW} ENABLE=1
|
||||
{% endif %}
|
||||
{% if cfg.done %}
|
||||
RESPOND MSG="PURGE_UNTIL_EMPTY: no more filament after {cfg.count} purge{'s' if cfg.count != 1 else ''}. The last bit between the extruder gears and the nozzle is pushed out by the next load."
|
||||
{% else %}
|
||||
RESPOND TYPE=error MSG="PURGE_UNTIL_EMPTY: still pressure after {params.MAX} purges. The lane may still be feeding, or the nozzle is blocked - check before running it again."
|
||||
{% endif %}
|
||||
@@ -0,0 +1,350 @@
|
||||
# https://github.com/tanaes/whopping_Voron_mods/blob/main/LEDs/Rainbow_Barf_Logo_LED/Code/stealthburner_led_effects_barf.cfg
|
||||
# Macros for setting the status leds on the Voron StealthBurner toolhead (or for any neopixel-type leds).
|
||||
#
|
||||
# You will need to configure a neopixel (or other addressable led, such as dotstar). See
|
||||
# https://www.klipper3d.org/Config_Reference.html#neopixel for configuration details.
|
||||
|
||||
|
||||
#####################################
|
||||
# INSTRUCTIONS #
|
||||
#####################################
|
||||
# How to use all this stuff:
|
||||
#
|
||||
# 1. Copy this .cfg file into your Klipper config directory and then add [include stealthburner_leds.cfg]
|
||||
# to the top of your printer.cfg in order for register the LEDs and macros with Klipper.
|
||||
# 2. Define your LEDs by editing [neopixel sb_leds] below and entering the data pin from your control board
|
||||
# as well as the color order.
|
||||
#
|
||||
# Note: RGB and RGBW are different and must be defined explicitly. RGB and RGBW are also not able to
|
||||
# be mix-and-matched in the same chain. A separate data line would be needed for proper functioning.
|
||||
#
|
||||
# RGBW LEDs will have a visible yellow-ish phosphor section to the chip. If your LEDs do not have
|
||||
# this yellow portion, you have RGB LEDs.
|
||||
#
|
||||
# 3. Save your config and restart Klipper.
|
||||
#
|
||||
# Note: We set RED and BLUE to 1.0 to make it easier for users and supporters to detect
|
||||
# misconfigurations or miswiring. The default color format is for Neopixels with a dedicated
|
||||
# white LED. On startup, all three SB LEDs should light up.
|
||||
#
|
||||
# If you get random colors across your LEDs, change the color_order to GRB and restart. Then
|
||||
# omit the W for each suggested color_order in the next paragraph.
|
||||
#
|
||||
# If you get MAGENTA, your color order is correct. If you get CYAN, you need to use RGBW. If
|
||||
# you get YELLOW, you need to use BRGW (note that BRG is only supported in the latest Klipper
|
||||
# version).
|
||||
#
|
||||
# 4. Once you have confirmed that the LEDs are set up correctly, you must now decide where you want
|
||||
# these macros called up...which means adding them to your existing gcode macros. NOTHING will happen
|
||||
# unless you add the STATUS_????? macros to your existing gcode macros.
|
||||
#
|
||||
# Example: add STATUS_LEVELING to the beginning of your QGL gcode macro, and then add STATUS_READY
|
||||
# to the end of it to set the logo LED and nozzle LEDs back to the `ready` state.
|
||||
#
|
||||
# Example: add STATUS_CLEANING to the beginning of your nozzle-cleaning macro, and then STATUS_READY
|
||||
# to the end of it to return the LEDs back to `ready` state.
|
||||
#
|
||||
# 5. Feel free to change colors of each macro, create new ones if you have a need to. The macros provided below
|
||||
# are just an example of what is possible. If you want to try some more complex animations, you will most
|
||||
# likely have to use WLED with Moonraker and a small micro-controller (please see the LED thread for help inside
|
||||
# of the stealthburner_beta channel on Discord).
|
||||
#
|
||||
#####################################
|
||||
# END INSTRUCTRUCTIONS #
|
||||
#####################################
|
||||
|
||||
|
||||
[neopixel sb_leds]
|
||||
pin: nhk:PD3
|
||||
# The pin connected to the neopixel. This parameter must be provided.
|
||||
chain_count: 10
|
||||
# The number of Neopixel chips that are "daisy chained" to the
|
||||
# provided pin. The default is 1 (which indicates only a single
|
||||
# Neopixel is connected to the pin).
|
||||
# color_order: GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRBW, GRBW
|
||||
color_order: GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRB, GRB
|
||||
# Set the pixel order required by the LED hardware. Options are GRB,
|
||||
# RGB, GRBW, or RGBW. The default is GRB.
|
||||
initial_RED: 1.0
|
||||
initial_GREEN: 0.0
|
||||
initial_BLUE: 1.0
|
||||
initial_WHITE: 0.0
|
||||
# Sets the initial LED color of the Neopixel. Each value should be
|
||||
# between 0.0 and 1.0. The WHITE option is only available on RGBW
|
||||
# LEDs. The default for each color is 0.#
|
||||
|
||||
|
||||
##########################
|
||||
# LED Effects Animations #
|
||||
##########################
|
||||
|
||||
##################
|
||||
## logo effects ##
|
||||
##################
|
||||
|
||||
[led_effect sb_logo_busy]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
layers:
|
||||
breathing 3 1 top (1,0,0)
|
||||
|
||||
[led_effect sb_logo_cleaning]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
layers:
|
||||
breathing 3 1 top (0.0, 0.02, 0.5)
|
||||
|
||||
[led_effect sb_logo_calibrating_z]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
layers:
|
||||
breathing 3 1 top (0.0, 0.0, 0.35)
|
||||
|
||||
[led_effect sb_logo_heating]
|
||||
leds:
|
||||
neopixel:sb_leds (2,3,7,8,6,1)
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
layers:
|
||||
comet 0.3 0 add (1, 0.18, 0)
|
||||
|
||||
[led_effect sb_logo_cooling]
|
||||
leds:
|
||||
neopixel:sb_leds (2,3,7,8,6,1)
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
layers:
|
||||
comet 0.3 0 add (0, 0, 1)
|
||||
|
||||
[led_effect sb_logo_homing]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
layers:
|
||||
breathing 3 1 top (0.0, 0.6, 0.2)
|
||||
|
||||
[led_effect sb_logo_leveling]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
layers:
|
||||
breathing 3 1 top (0.5, 0.1, 0.4)
|
||||
|
||||
[led_effect sb_logo_meshing]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
layers:
|
||||
breathing 3 1 top (0.2, 1.0, 0.0)
|
||||
|
||||
[led_effect sb_logo_printing]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
layers:
|
||||
gradient 0.3 1 add (0.3, 0.0, 0.0),(0.3, 0.3, 0.0),(0.3, 0.1, 0.0)
|
||||
|
||||
[led_effect sb_logo_standby]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
layers:
|
||||
breathing 3 1 top (0.01, 0.01, 0.01)
|
||||
|
||||
[led_effect sb_logo_part_ready]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
layers:
|
||||
breathing 3 1 top (0.0, 1.0, 0.0)
|
||||
|
||||
####################
|
||||
## nozzle effects ##
|
||||
####################
|
||||
|
||||
[led_effect sb_nozzle_heating]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (9,10)
|
||||
layers:
|
||||
breathing 3 1 top (1.0, 0.18, 0.0, 0.0)
|
||||
|
||||
[led_effect sb_nozzle_cooling]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (9,10)
|
||||
layers:
|
||||
breathing 3 1 top (0.0, 0.0, 1.0, 0.1)
|
||||
|
||||
[led_effect sb_nozzle_standby]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (9,10)
|
||||
layers:
|
||||
breathing 3 1 top (0.6, 0.0, 0.0, 0.0)
|
||||
|
||||
[led_effect sb_nozzle_part_ready]
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
leds:
|
||||
neopixel:sb_leds (9,10)
|
||||
layers:
|
||||
breathing 3 1 top (0.6, 1.0, 0.0, 0.1)
|
||||
|
||||
|
||||
#####################
|
||||
## all led effects ##
|
||||
#####################
|
||||
|
||||
[led_effect sb_critical_error]
|
||||
leds:
|
||||
neopixel:sb_leds
|
||||
layers:
|
||||
strobe 1 1.5 add (1.0, 1.0, 1.0)
|
||||
breathing 2 0 difference (0.95, 0.0, 0.0)
|
||||
static 1 0 top (1.0, 0.0, 0.0)
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
run_on_error: true
|
||||
|
||||
|
||||
[led_effect rainbow]
|
||||
leds:
|
||||
neopixel:sb_leds
|
||||
autostart: true
|
||||
frame_rate: 24
|
||||
layers:
|
||||
gradient 0.3 1 add (0.3, 0.0, 0.0),(0.0, 0.3, 0.0),(0.0, 0.0, 0.3)
|
||||
|
||||
|
||||
#######################
|
||||
# LED Effects Statics #
|
||||
#######################
|
||||
|
||||
[led_effect set_nozzle_leds]
|
||||
leds:
|
||||
neopixel:sb_leds (9,10)
|
||||
#neopixel:caselight
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
layers:
|
||||
# static 0 0 top (0.0, 0.0, 0.0, 1.0)
|
||||
static 0 0 top (1.0, 1.0, 1.0, 1.0)
|
||||
|
||||
[led_effect set_logo_leds]
|
||||
leds:
|
||||
neopixel:sb_leds (2,1,3,4,5,6,7,8)
|
||||
#neopixel:caselight
|
||||
autostart: false
|
||||
frame_rate: 24
|
||||
layers:
|
||||
static 0 0 top (1.0, 1.0, 1.0)
|
||||
|
||||
##############
|
||||
# The Macros #
|
||||
##############
|
||||
|
||||
[gcode_macro _set_logo_leds_off]
|
||||
gcode:
|
||||
SET_LED_EFFECT EFFECT=set_logo_leds STOP=1
|
||||
|
||||
[gcode_macro _set_logo_leds_on]
|
||||
gcode:
|
||||
SET_LED_EFFECT EFFECT=set_logo_leds
|
||||
|
||||
[gcode_macro _set_nozzle_leds_on]
|
||||
gcode:
|
||||
SET_LED_EFFECT EFFECT=set_nozzle_leds
|
||||
|
||||
[gcode_macro _set_nozzle_leds_off]
|
||||
gcode:
|
||||
SET_LED_EFFECT EFFECT=set_nozzle_leds STOP=1
|
||||
|
||||
# The _status_* macros only stop the toolhead effects (neopixel:sb_leds), so
|
||||
# the Box Turtle logo (rainbow_turtle on neopixel:bt_logo) keeps running during
|
||||
# prints. _status_off still stops everything; _status_ready starts the logo again.
|
||||
[gcode_macro _status_off]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS
|
||||
|
||||
[gcode_macro _status_ready]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=rainbow
|
||||
SET_LED_EFFECT EFFECT=rainbow_turtle
|
||||
|
||||
[gcode_macro _status_part_ready]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_nozzle_part_ready
|
||||
SET_LED_EFFECT EFFECT=sb_logo_part_ready
|
||||
|
||||
[gcode_macro _status_busy]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_logo_busy
|
||||
_set_nozzle_leds_on
|
||||
|
||||
[gcode_macro _status_heating]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_logo_heating
|
||||
SET_LED_EFFECT EFFECT=sb_nozzle_heating
|
||||
|
||||
[gcode_macro _status_cooling]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_logo_cooling
|
||||
SET_LED_EFFECT EFFECT=sb_nozzle_cooling
|
||||
|
||||
|
||||
[gcode_macro _status_leveling]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_logo_leveling
|
||||
_set_nozzle_leds_on
|
||||
|
||||
[gcode_macro _status_homing]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_logo_homing
|
||||
_set_nozzle_leds_on
|
||||
|
||||
[gcode_macro _status_cleaning]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_logo_cleaning
|
||||
_set_nozzle_leds_on
|
||||
|
||||
[gcode_macro _status_meshing]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_logo_meshing
|
||||
_set_nozzle_leds_on
|
||||
|
||||
[gcode_macro _status_calibrating_z]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_logo_calibrating_z
|
||||
_set_nozzle_leds_on
|
||||
|
||||
[gcode_macro _status_printing]
|
||||
gcode:
|
||||
STOP_LED_EFFECTS LEDS="neopixel:sb_leds"
|
||||
SET_LED_EFFECT EFFECT=sb_logo_printing
|
||||
_set_nozzle_leds_on
|
||||
@@ -0,0 +1,88 @@
|
||||
# MCU firmware update scripts
|
||||
|
||||
| Script | Does |
|
||||
|---|---|
|
||||
| `update_all_mcus.sh` | Pulls Klipper, builds and flashes **Klipper** to every MCU, prints a summary, restarts Klipper |
|
||||
| `update_katapult_all_mcus.sh` | Builds and flashes **Katapult** (the bootloader) to every MCU |
|
||||
|
||||
Based on the [Voron guide to automating MCU updates](https://docs.vorondesign.com/community/howto/drachenkatze/automating_klipper_mcu_updates.html)
|
||||
and [Esoterical's CANBus guide](https://canbus.esoterical.online/).
|
||||
|
||||
Run them on the Klipper host, between prints:
|
||||
|
||||
```bash
|
||||
~/klipper-macros/scripts/update_all_mcus.sh
|
||||
```
|
||||
|
||||
They stop Klipper while flashing (and need `sudo` for that).
|
||||
|
||||
## These scripts are for one printer
|
||||
|
||||
They contain the original printer's boards, build configs, CAN UUIDs and
|
||||
serial IDs:
|
||||
|
||||
| Board | Name in the script | Connection | Build config (`KCONFIG_CONFIG=`) |
|
||||
|---|---|---|---|
|
||||
| LDO Leviathan (CAN bridge) | `Main MCU` | CAN `5a2b9f704f54` | `leviathan` |
|
||||
| BoxTurtle MC (Pi Pico) | `MC_1 (BoxTurtle)` | CAN `acc4091027b4` | `BoxTurtleMC` |
|
||||
| BTT EBB42 | `EBB42` | CAN `4b5324a91849` | `EBB42` |
|
||||
| AFC-Lite (Box Turtle) | `Turtle_1` | CAN `59a2feeada77` | `Turtle_1` |
|
||||
| LDO Nitehawk-SB | `NHK` | USB `usb-Klipper_stm32g0b1xx_550039000A50315058393320-if00` | `nhk` |
|
||||
| iHeater | `iHeater` | USB `usb-Klipper_stm32f042x6_0C0020000E43304754393320-if00` | `iHeater` |
|
||||
|
||||
**Running them unchanged on another printer will not work, and flashing the
|
||||
wrong firmware to a board can leave it unusable until it is reflashed by hand.**
|
||||
|
||||
## Adapting them to your printer
|
||||
|
||||
1. **Make one build config per board.** In `~/klipper` (and in `~/katapult`
|
||||
for the Katapult script), run `make menuconfig KCONFIG_CONFIG=<name>` for
|
||||
each board, with the settings from your board's documentation. The file
|
||||
`<name>` is saved in that folder and the script uses it to build.
|
||||
|
||||
2. **Find your IDs.**
|
||||
- CAN boards: `~/katapult/scripts/flashtool.py -i can0 -q`, or the
|
||||
`canbus_uuid` values in `printer.cfg`.
|
||||
- USB boards: `ls /dev/serial/by-id/`, or the `serial:` values in
|
||||
`printer.cfg`.
|
||||
|
||||
3. **Edit the flash blocks.** Each board has one block like this:
|
||||
|
||||
```bash
|
||||
printf "Update EBB42 firmware\n"
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=EBB42
|
||||
make KCONFIG_CONFIG=EBB42
|
||||
flash_device "EBB42" python3 ~/katapult/scripts/flashtool.py -i can0 -f ~/klipper/out/klipper.bin -u 4b5324a91849
|
||||
```
|
||||
|
||||
For each of your boards, set the name, the `KCONFIG_CONFIG` and the
|
||||
flash command:
|
||||
- CAN board via Katapult: `... flashtool.py -i can0 -f <bin> -u <uuid>`
|
||||
- USB board via Katapult: `... flashtool.py -f <bin> -d /dev/serial/by-id/<id>`
|
||||
- USB board via Klipper's own flashing: `make flash KCONFIG_CONFIG=<name> FLASH_DEVICE=/dev/serial/by-id/<id>`
|
||||
|
||||
Delete the blocks of boards you do not have.
|
||||
|
||||
4. **Keep the CAN bridge last.** If your mainboard is a USB-to-CAN bridge,
|
||||
flash it after all CAN boards: while it reboots, the CAN bus is gone.
|
||||
|
||||
5. **Update the names in `print_summary`.** The list
|
||||
`for name in "Main MCU" "MC_1 (BoxTurtle)" ...` must contain the same names
|
||||
as your `flash_device` calls, otherwise they show as SKIPPED.
|
||||
|
||||
6. **Katapult script:** Katapult can only replace itself if you build it as
|
||||
a *deployment application* ("Build Katapult deployment application" in
|
||||
`make menuconfig`); a normal Katapult build flashed through Katapult
|
||||
overwrites the application slot instead. Check the Katapult docs for your
|
||||
board before using that script, and keep a way to flash over USB/DFU.
|
||||
|
||||
The `CAN_DEVICES` / `CAN_DEVICE_ORDER` lists at the top of the scripts are
|
||||
not used by the flashing itself; they only document the UUIDs.
|
||||
|
||||
## Known differences between the two scripts
|
||||
|
||||
`update_katapult_all_mcus.sh` ends after the last board: it does not print
|
||||
the summary and does not start Klipper again. Run `sudo systemctl start
|
||||
klipper` afterwards, or add the last lines of `update_all_mcus.sh`
|
||||
(`print_summary` and the restart) to it.
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/bin/bash
|
||||
# https://docs.vorondesign.com/community/howto/drachenkatze/automating_klipper_mcu_updates.html
|
||||
# https://canbus.esoterical.online/mainboard_klipper_updating.html
|
||||
|
||||
# Status tracking
|
||||
declare -A FLASH_STATUS
|
||||
declare -A FLASH_OUTPUT
|
||||
|
||||
# CAN devices to check (name -> uuid) — CAN BRIDGE MUST BE LAST
|
||||
declare -A CAN_DEVICES
|
||||
declare -a CAN_DEVICE_ORDER # preserve order since bash associative arrays don't
|
||||
CAN_DEVICES["MC_1 (BoxTurtle)"]="acc4091027b4"
|
||||
CAN_DEVICES["EBB42"]="4b5324a91849"
|
||||
CAN_DEVICES["Turtle_1"]="59a2feeada77"
|
||||
CAN_DEVICES["Main MCU"]="5a2b9f704f54" # CAN bridge — LAST
|
||||
CAN_DEVICE_ORDER=("MC_1 (BoxTurtle)" "EBB42" "Turtle_1" "Main MCU")
|
||||
# NHK and iHeater are serial devices, not CAN — skipped from CAN check
|
||||
|
||||
flash_device() {
|
||||
local name="$1"
|
||||
shift
|
||||
local output
|
||||
output=$("$@" 2>&1)
|
||||
local exit_code=$?
|
||||
FLASH_OUTPUT["$name"]="$output"
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
FLASH_STATUS["$name"]="✅ SUCCESS"
|
||||
else
|
||||
FLASH_STATUS["$name"]="❌ FAILED (exit $exit_code)"
|
||||
fi
|
||||
echo "$output"
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
printf "\n========================================\n"
|
||||
printf " FIRMWARE UPDATE SUMMARY\n"
|
||||
printf "========================================\n"
|
||||
local all_ok=true
|
||||
for name in "Main MCU" "MC_1 (BoxTurtle)" "NHK" "EBB42" "Turtle_1" "iHeater"; do
|
||||
local status="${FLASH_STATUS[$name]:-⚠️ SKIPPED}"
|
||||
printf "%-20s %s\n" "$name" "$status"
|
||||
if [[ "$status" != *"SUCCESS"* ]]; then
|
||||
all_ok=false
|
||||
fi
|
||||
done
|
||||
printf "========================================\n"
|
||||
if $all_ok; then
|
||||
printf "All devices updated successfully! 🎉\n"
|
||||
else
|
||||
printf "Some devices failed or were skipped.\n"
|
||||
printf "\nFailed device details:\n"
|
||||
for name in "Main MCU" "MC_1 (BoxTurtle)" "NHK" "EBB42" "Turtle_1" "iHeater"; do
|
||||
if [[ "${FLASH_STATUS[$name]}" == *"FAILED"* ]]; then
|
||||
printf "\n--- %s ---\n" "$name"
|
||||
printf "%s\n" "${FLASH_OUTPUT[$name]}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
printf "========================================\n"
|
||||
}
|
||||
|
||||
printf "Pulling klipper changes\n"
|
||||
sudo service klipper stop
|
||||
cd ~/klipper
|
||||
git pull
|
||||
|
||||
printf "\nUpdate MC_1 firmware\n"
|
||||
# MC_1
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=BoxTurtleMC
|
||||
make KCONFIG_CONFIG=BoxTurtleMC
|
||||
flash_device "MC_1 (BoxTurtle)" python3 ~/katapult/scripts/flashtool.py -i can0 -f ~/klipper/out/klipper.bin -u acc4091027b4
|
||||
|
||||
printf "Update nhk firmware\n"
|
||||
# nhk
|
||||
# https://docs.ldomotors.com/en/Toolboard/nitehawk-sb-v2
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=nhk
|
||||
make KCONFIG_CONFIG=nhk
|
||||
flash_device "NHK" make flash KCONFIG_CONFIG=nhk FLASH_DEVICE=/dev/serial/by-id/usb-Klipper_stm32g0b1xx_550039000A50315058393320-if00
|
||||
|
||||
printf "Update EBB42 firmware\n"
|
||||
# EBB42
|
||||
# https://canbus.esoterical.online/toolhead_flashing/common_hardware/BigTreeTech%20EBB42%20V1.2/README.html
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=EBB42
|
||||
make KCONFIG_CONFIG=EBB42
|
||||
flash_device "EBB42" python3 ~/katapult/scripts/flashtool.py -i can0 -f ~/klipper/out/klipper.bin -u 4b5324a91849
|
||||
|
||||
printf "Update Turtle_1 firmware\n"
|
||||
# Turtle_1
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=Turtle_1
|
||||
make KCONFIG_CONFIG=Turtle_1
|
||||
flash_device "Turtle_1" python3 ~/katapult/scripts/flashtool.py -i can0 -f ~/klipper/out/klipper.bin -u 59a2feeada77
|
||||
|
||||
printf "Update Mcu firmware\n"
|
||||
# Main Mcu
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=leviathan
|
||||
make KCONFIG_CONFIG=leviathan
|
||||
flash_device "Main MCU" python3 ~/katapult/scripts/flashtool.py -i can0 -f ~/klipper/out/klipper.bin -u 5a2b9f704f54
|
||||
#flash_device "Main MCU" python3 ~/katapult/scripts/flashtool.py -f ~/klipper/out/klipper.bin -d /dev/serial/by-id/usb-Klipper_stm32f446xx_3B0017000251303532383235-if00
|
||||
|
||||
printf "Update iHeater firmware\n"
|
||||
# iHeater
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=iHeater
|
||||
make KCONFIG_CONFIG=iHeater
|
||||
flash_device "iHeater" python3 ~/katapult/scripts/flashtool.py -f ~/klipper/out/klipper.bin -d /dev/serial/by-id/usb-Klipper_stm32f042x6_0C0020000E43304754393320-if00
|
||||
|
||||
print_summary
|
||||
|
||||
printf "\nStarting klipper\n"
|
||||
printf "%s " "Press enter to continue"
|
||||
read ans
|
||||
sudo service klipper start
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# https://docs.vorondesign.com/community/howto/drachenkatze/automating_klipper_mcu_updates.html
|
||||
# https://canbus.esoterical.online/mainboard_klipper_updating.html
|
||||
|
||||
# Status tracking
|
||||
declare -A FLASH_STATUS
|
||||
declare -A FLASH_OUTPUT
|
||||
|
||||
# CAN devices to check (name -> uuid) — CAN BRIDGE MUST BE LAST
|
||||
declare -A CAN_DEVICES
|
||||
declare -a CAN_DEVICE_ORDER # preserve order since bash associative arrays don't
|
||||
CAN_DEVICES["MC_1 (BoxTurtle)"]="acc4091027b4"
|
||||
CAN_DEVICES["EBB42"]="4b5324a91849"
|
||||
CAN_DEVICES["Turtle_1"]="59a2feeada77"
|
||||
CAN_DEVICES["Main MCU"]="5a2b9f704f54" # CAN bridge — LAST
|
||||
CAN_DEVICE_ORDER=("MC_1 (BoxTurtle)" "EBB42" "Turtle_1" "Main MCU")
|
||||
# NHK and iHeater are serial devices, not CAN — skipped from CAN check
|
||||
|
||||
flash_device() {
|
||||
local name="$1"
|
||||
shift
|
||||
local output
|
||||
output=$("$@" 2>&1)
|
||||
local exit_code=$?
|
||||
FLASH_OUTPUT["$name"]="$output"
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
FLASH_STATUS["$name"]="✅ SUCCESS"
|
||||
else
|
||||
FLASH_STATUS["$name"]="❌ FAILED (exit $exit_code)"
|
||||
fi
|
||||
echo "$output"
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
printf "\n========================================\n"
|
||||
printf " FIRMWARE UPDATE SUMMARY\n"
|
||||
printf "========================================\n"
|
||||
local all_ok=true
|
||||
for name in "Main MCU" "MC_1 (BoxTurtle)" "NHK" "EBB42" "Turtle_1" "iHeater"; do
|
||||
local status="${FLASH_STATUS[$name]:-⚠️ SKIPPED}"
|
||||
printf "%-20s %s\n" "$name" "$status"
|
||||
if [[ "$status" != *"SUCCESS"* ]]; then
|
||||
all_ok=false
|
||||
fi
|
||||
done
|
||||
printf "========================================\n"
|
||||
if $all_ok; then
|
||||
printf "All devices updated successfully! 🎉\n"
|
||||
else
|
||||
printf "Some devices failed or were skipped.\n"
|
||||
printf "\nFailed device details:\n"
|
||||
for name in "Main MCU" "MC_1 (BoxTurtle)" "NHK" "EBB42" "Turtle_1" "iHeater"; do
|
||||
if [[ "${FLASH_STATUS[$name]}" == *"FAILED"* ]]; then
|
||||
printf "\n--- %s ---\n" "$name"
|
||||
printf "%s\n" "${FLASH_OUTPUT[$name]}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
printf "========================================\n"
|
||||
}
|
||||
|
||||
printf "Pulling klipper changes\n"
|
||||
sudo service klipper stop
|
||||
cd ~/katapult
|
||||
|
||||
printf "\nUpdate MC_1 firmware\n"
|
||||
# MC_1
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=BoxTurtleMC
|
||||
make KCONFIG_CONFIG=BoxTurtleMC
|
||||
flash_device "MC_1 (BoxTurtle)" python3 ~/katapult/scripts/flashtool.py -i can0 -f ~/katapult/out/katapult.bin -u acc4091027b4
|
||||
|
||||
printf "Update nhk firmware\n"
|
||||
# nhk
|
||||
# https://docs.ldomotors.com/en/Toolboard/nitehawk-sb-v2
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=nhk
|
||||
make KCONFIG_CONFIG=nhk
|
||||
flash_device "NHK" make flash KCONFIG_CONFIG=nhk FLASH_DEVICE=/dev/serial/by-id/usb-Klipper_stm32g0b1xx_550039000A50315058393320-if00
|
||||
|
||||
printf "Update EBB42 firmware\n"
|
||||
# EBB42
|
||||
# https://canbus.esoterical.online/toolhead_flashing/common_hardware/BigTreeTech%20EBB42%20V1.2/README.html
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=EBB42
|
||||
make KCONFIG_CONFIG=EBB42
|
||||
flash_device "EBB42" python3 ~/katapult/scripts/flashtool.py -i can0 -f ~/katapult/out/katapult.bin -u 4b5324a91849
|
||||
|
||||
printf "Update Turtle_1 firmware\n"
|
||||
# Turtle_1
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=Turtle_1
|
||||
make KCONFIG_CONFIG=Turtle_1
|
||||
flash_device "Turtle_1" python3 ~/katapult/scripts/flashtool.py -i can0 -f ~/katapult/out/katapult.bin -u 59a2feeada77
|
||||
|
||||
printf "Update Mcu firmware\n"
|
||||
# Main Mcu
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=leviathan
|
||||
make KCONFIG_CONFIG=leviathan
|
||||
flash_device "Main MCU" python3 ~/katapult/scripts/flashtool.py -i can0 -f ~/katapult/out/katapult.bin -u 5a2b9f704f54
|
||||
#flash_device "Main MCU" python3 ~/katapult/scripts/flashtool.py -f ~/katapult/out/katapult.bin -d /dev/serial/by-id/usb-Klipper_stm32f446xx_3B0017000251303532383235-if00
|
||||
|
||||
printf "Update iHeater firmware\n"
|
||||
# iHeater
|
||||
make clean
|
||||
make clean KCONFIG_CONFIG=iHeater
|
||||
make KCONFIG_CONFIG=iHeater
|
||||
flash_device "iHeater" python3 ~/katapult/scripts/flashtool.py -f ~/katapult/out/katapult.bin -d /dev/serial/by-id/usb-Klipper_stm32f042x6_0C0020000E43304754393320-if00
|
||||
Reference in new issue
Block a user