Initial import of my Klipper macros, modules and scripts

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

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

No files matched your search

+107
View File
@@ -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)
+71
View File
@@ -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)
+104
View File
@@ -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)