klipper update

This commit is contained in:
Rainboooom
2023-06-15 11:41:08 +08:00
parent 845d13acb1
commit dffff1ae35
1921 changed files with 1625400 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
# Package definition for the extras directory
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.

BIN
klippy/extras/__init__.pyc Normal file

Binary file not shown.

22
klippy/extras/ad5206.py Normal file
View File

@@ -0,0 +1,22 @@
# AD5206 digipot code
#
# Copyright (C) 2017,2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import bus
class ad5206:
def __init__(self, config):
self.spi = bus.MCU_SPI_from_config(
config, 0, pin_option="enable_pin", default_speed=25000000)
scale = config.getfloat('scale', 1., above=0.)
for i in range(6):
val = config.getfloat('channel_%d' % (i+1,), None,
minval=0., maxval=scale)
if val is not None:
self.set_register(i, int(val * 256. / scale + .5))
def set_register(self, reg, value):
self.spi.spi_send([reg, value])
def load_config_prefix(config):
return ad5206(config)

View File

@@ -0,0 +1,79 @@
# Support for scaling ADC values based on measured VREF and VSSA
#
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
SAMPLE_TIME = 0.001
SAMPLE_COUNT = 8
REPORT_TIME = 0.300
RANGE_CHECK_COUNT = 4
class MCU_scaled_adc:
def __init__(self, main, pin_params):
self._main = main
self._last_state = (0., 0.)
self._mcu_adc = main.mcu.setup_pin('adc', pin_params)
query_adc = main.printer.lookup_object('query_adc')
qname = main.name + ":" + pin_params['pin']
query_adc.register_adc(qname, self._mcu_adc)
self._callback = None
self.setup_minmax = self._mcu_adc.setup_minmax
self.get_mcu = self._mcu_adc.get_mcu
def _handle_callback(self, read_time, read_value):
max_adc = self._main.last_vref[1]
min_adc = self._main.last_vssa[1]
scaled_val = (read_value - min_adc) / (max_adc - min_adc)
self._last_state = (scaled_val, read_time)
self._callback(read_time, scaled_val)
def setup_adc_callback(self, report_time, callback):
self._callback = callback
self._mcu_adc.setup_adc_callback(report_time, self._handle_callback)
def get_last_value(self):
return self._last_state
class PrinterADCScaled:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[1]
self.last_vref = (0., 0.)
self.last_vssa = (0., 0.)
# Configure vref and vssa pins
self.mcu_vref = self._config_pin(config, 'vref', self.vref_callback)
self.mcu_vssa = self._config_pin(config, 'vssa', self.vssa_callback)
smooth_time = config.getfloat('smooth_time', 2., above=0.)
self.inv_smooth_time = 1. / smooth_time
self.mcu = self.mcu_vref.get_mcu()
if self.mcu is not self.mcu_vssa.get_mcu():
raise config.error("vref and vssa must be on same mcu")
# Register setup_pin
ppins = self.printer.lookup_object('pins')
ppins.register_chip(self.name, self)
def _config_pin(self, config, name, callback):
pin_name = config.get(name + '_pin')
ppins = self.printer.lookup_object('pins')
mcu_adc = ppins.setup_pin('adc', pin_name)
mcu_adc.setup_adc_callback(REPORT_TIME, callback)
mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT, minval=0., maxval=1.,
range_check_count=RANGE_CHECK_COUNT)
query_adc = config.get_printer().load_object(config, 'query_adc')
query_adc.register_adc(self.name + ":" + name, mcu_adc)
return mcu_adc
def setup_pin(self, pin_type, pin_params):
if pin_type != 'adc':
raise self.printer.config_error("adc_scaled only supports adc pins")
return MCU_scaled_adc(self, pin_params)
def calc_smooth(self, read_time, read_value, last):
last_time, last_value = last
time_diff = read_time - last_time
value_diff = read_value - last_value
adj_time = min(time_diff * self.inv_smooth_time, 1.)
smoothed_value = last_value + value_diff * adj_time
return (read_time, smoothed_value)
def vref_callback(self, read_time, read_value):
self.last_vref = self.calc_smooth(read_time, read_value, self.last_vref)
def vssa_callback(self, read_time, read_value):
self.last_vssa = self.calc_smooth(read_time, read_value, self.last_vssa)
def load_config_prefix(config):
return PrinterADCScaled(config)

View File

@@ -0,0 +1,302 @@
# Obtain temperature using linear interpolation of ADC values
#
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, bisect
######################################################################
# Interface between MCU adc and heater temperature callbacks
######################################################################
SAMPLE_TIME = 0.001
SAMPLE_COUNT = 8
REPORT_TIME = 0.300
RANGE_CHECK_COUNT = 4
# Interface between ADC and heater temperature callbacks
class PrinterADCtoTemperature:
def __init__(self, config, adc_convert):
self.adc_convert = adc_convert
ppins = config.get_printer().lookup_object('pins')
self.mcu_adc = ppins.setup_pin('adc', config.get('sensor_pin'))
self.mcu_adc.setup_adc_callback(REPORT_TIME, self.adc_callback)
query_adc = config.get_printer().load_object(config, 'query_adc')
query_adc.register_adc(config.get_name(), self.mcu_adc)
def setup_callback(self, temperature_callback):
self.temperature_callback = temperature_callback
def get_report_time_delta(self):
return REPORT_TIME
def adc_callback(self, read_time, read_value):
temp = self.adc_convert.calc_temp(read_value)
self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp)
def setup_minmax(self, min_temp, max_temp):
adc_range = [self.adc_convert.calc_adc(t) for t in [min_temp, max_temp]]
self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT,
minval=min(adc_range), maxval=max(adc_range),
range_check_count=RANGE_CHECK_COUNT)
######################################################################
# Linear interpolation
######################################################################
# Helper code to perform linear interpolation
class LinearInterpolate:
def __init__(self, samples):
self.keys = []
self.slopes = []
last_key = last_value = None
for key, value in sorted(samples):
if last_key is None:
last_key = key
last_value = value
continue
if key <= last_key:
raise ValueError("duplicate value")
gain = (value - last_value) / (key - last_key)
offset = last_value - last_key * gain
if self.slopes and self.slopes[-1] == (gain, offset):
continue
last_value = value
last_key = key
self.keys.append(key)
self.slopes.append((gain, offset))
if not self.keys:
raise ValueError("need at least two samples")
self.keys.append(9999999999999.)
self.slopes.append(self.slopes[-1])
def interpolate(self, key):
pos = bisect.bisect(self.keys, key)
gain, offset = self.slopes[pos]
return key * gain + offset
def reverse_interpolate(self, value):
values = [key * gain + offset for key, (gain, offset) in zip(
self.keys, self.slopes)]
if values[0] < values[-2]:
valid = [i for i in range(len(values)) if values[i] >= value]
else:
valid = [i for i in range(len(values)) if values[i] <= value]
gain, offset = self.slopes[min(valid + [len(values) - 1])]
return (value - offset) / gain
######################################################################
# Linear voltage to temperature converter
######################################################################
# Linear style conversion chips calibrated from temperature measurements
class LinearVoltage:
def __init__(self, config, params):
adc_voltage = config.getfloat('adc_voltage', 5., above=0.)
voltage_offset = config.getfloat('voltage_offset', 0.0)
samples = []
for temp, volt in params:
adc = (volt - voltage_offset) / adc_voltage
if adc < 0. or adc > 1.:
logging.warn("Ignoring adc sample %.3f/%.3f in heater %s",
temp, volt, config.get_name())
continue
samples.append((adc, temp))
try:
li = LinearInterpolate(samples)
except ValueError as e:
raise config.error("adc_temperature %s in heater %s" % (
str(e), config.get_name()))
self.calc_temp = li.interpolate
self.calc_adc = li.reverse_interpolate
# Custom defined sensors from the config file
class CustomLinearVoltage:
def __init__(self, config):
self.name = " ".join(config.get_name().split()[1:])
self.params = []
for i in range(1, 1000):
t = config.getfloat("temperature%d" % (i,), None)
if t is None:
break
v = config.getfloat("voltage%d" % (i,))
self.params.append((t, v))
def create(self, config):
lv = LinearVoltage(config, self.params)
return PrinterADCtoTemperature(config, lv)
######################################################################
# Linear resistance to temperature converter
######################################################################
# Linear resistance calibrated from temperature measurements
class LinearResistance:
def __init__(self, config, samples):
self.pullup = config.getfloat('pullup_resistor', 4700., above=0.)
try:
self.li = LinearInterpolate([(r, t) for t, r in samples])
except ValueError as e:
raise config.error("adc_temperature %s in heater %s" % (
str(e), config.get_name()))
def calc_temp(self, adc):
# Calculate temperature from adc
adc = max(.00001, min(.99999, adc))
r = self.pullup * adc / (1.0 - adc)
return self.li.interpolate(r)
def calc_adc(self, temp):
# Calculate adc reading from a temperature
r = self.li.reverse_interpolate(temp)
return r / (self.pullup + r)
# Custom defined sensors from the config file
class CustomLinearResistance:
def __init__(self, config):
self.name = " ".join(config.get_name().split()[1:])
self.samples = []
for i in range(1, 1000):
t = config.getfloat("temperature%d" % (i,), None)
if t is None:
break
r = config.getfloat("resistance%d" % (i,))
self.samples.append((t, r))
def create(self, config):
lr = LinearResistance(config, self.samples)
return PrinterADCtoTemperature(config, lr)
######################################################################
# Default sensors
######################################################################
AD595 = [
(0., .0027), (10., .101), (20., .200), (25., .250), (30., .300),
(40., .401), (50., .503), (60., .605), (80., .810), (100., 1.015),
(120., 1.219), (140., 1.420), (160., 1.620), (180., 1.817), (200., 2.015),
(220., 2.213), (240., 2.413), (260., 2.614), (280., 2.817), (300., 3.022),
(320., 3.227), (340., 3.434), (360., 3.641), (380., 3.849), (400., 4.057),
(420., 4.266), (440., 4.476), (460., 4.686), (480., 4.896)
]
AD597 = [
(0., 0.), (10., .097), (20., .196), (25., .245), (30., .295),
(40., 0.395), (50., 0.496), (60., 0.598), (80., 0.802), (100., 1.005),
(120., 1.207), (140., 1.407), (160., 1.605), (180., 1.801), (200., 1.997),
(220., 2.194), (240., 2.392), (260., 2.592), (280., 2.794), (300., 2.996),
(320., 3.201), (340., 3.406), (360., 3.611), (380., 3.817), (400., 4.024),
(420., 4.232), (440., 4.440), (460., 4.649), (480., 4.857), (500., 5.066)
]
AD8494 = [
(-180, -0.714), (-160, -0.658), (-140, -0.594), (-120, -0.523),
(-100, -0.446), (-80, -0.365), (-60, -0.278), (-40, -0.188),
(-20, -0.095), (0, 0.002), (20, 0.1), (25, 0.125), (40, 0.201),
(60, 0.303), (80, 0.406), (100, 0.511), (120, 0.617), (140, 0.723),
(160, 0.829), (180, 0.937), (200, 1.044), (220, 1.151), (240, 1.259),
(260, 1.366), (280, 1.473), (300, 1.58), (320, 1.687), (340, 1.794),
(360, 1.901), (380, 2.008), (400, 2.114), (420, 2.221), (440, 2.328),
(460, 2.435), (480, 2.542), (500, 2.65), (520, 2.759), (540, 2.868),
(560, 2.979), (580, 3.09), (600, 3.203), (620, 3.316), (640, 3.431),
(660, 3.548), (680, 3.666), (700, 3.786), (720, 3.906), (740, 4.029),
(760, 4.152), (780, 4.276), (800, 4.401), (820, 4.526), (840, 4.65),
(860, 4.774), (880, 4.897), (900, 5.018), (920, 5.138), (940, 5.257),
(960, 5.374), (980, 5.49), (1000, 5.606), (1020, 5.72), (1040, 5.833),
(1060, 5.946), (1080, 6.058), (1100, 6.17), (1120, 6.282), (1140, 6.394),
(1160, 6.505), (1180, 6.616), (1200, 6.727)
]
AD8495 = [
(-260, -0.786), (-240, -0.774), (-220, -0.751), (-200, -0.719),
(-180, -0.677), (-160, -0.627), (-140, -0.569), (-120, -0.504),
(-100, -0.432), (-80, -0.355), (-60, -0.272), (-40, -0.184), (-20, -0.093),
(0, 0.003), (20, 0.1), (25, 0.125), (40, 0.2), (60, 0.301), (80, 0.402),
(100, 0.504), (120, 0.605), (140, 0.705), (160, 0.803), (180, 0.901),
(200, 0.999), (220, 1.097), (240, 1.196), (260, 1.295), (280, 1.396),
(300, 1.497), (320, 1.599), (340, 1.701), (360, 1.803), (380, 1.906),
(400, 2.01), (420, 2.113), (440, 2.217), (460, 2.321), (480, 2.425),
(500, 2.529), (520, 2.634), (540, 2.738), (560, 2.843), (580, 2.947),
(600, 3.051), (620, 3.155), (640, 3.259), (660, 3.362), (680, 3.465),
(700, 3.568), (720, 3.67), (740, 3.772), (760, 3.874), (780, 3.975),
(800, 4.076), (820, 4.176), (840, 4.275), (860, 4.374), (880, 4.473),
(900, 4.571), (920, 4.669), (940, 4.766), (960, 4.863), (980, 4.959),
(1000, 5.055), (1020, 5.15), (1040, 5.245), (1060, 5.339), (1080, 5.432),
(1100, 5.525), (1120, 5.617), (1140, 5.709), (1160, 5.8), (1180, 5.891),
(1200, 5.98), (1220, 6.069), (1240, 6.158), (1260, 6.245), (1280, 6.332),
(1300, 6.418), (1320, 6.503), (1340, 6.587), (1360, 6.671), (1380, 6.754)
]
AD8496 = [
(-180, -0.642), (-160, -0.59), (-140, -0.53), (-120, -0.464),
(-100, -0.392), (-80, -0.315), (-60, -0.235), (-40, -0.15), (-20, -0.063),
(0, 0.027), (20, 0.119), (25, 0.142), (40, 0.213), (60, 0.308),
(80, 0.405), (100, 0.503), (120, 0.601), (140, 0.701), (160, 0.8),
(180, 0.9), (200, 1.001), (220, 1.101), (240, 1.201), (260, 1.302),
(280, 1.402), (300, 1.502), (320, 1.602), (340, 1.702), (360, 1.801),
(380, 1.901), (400, 2.001), (420, 2.1), (440, 2.2), (460, 2.3),
(480, 2.401), (500, 2.502), (520, 2.603), (540, 2.705), (560, 2.808),
(580, 2.912), (600, 3.017), (620, 3.124), (640, 3.231), (660, 3.34),
(680, 3.451), (700, 3.562), (720, 3.675), (740, 3.789), (760, 3.904),
(780, 4.02), (800, 4.137), (820, 4.254), (840, 4.37), (860, 4.486),
(880, 4.6), (900, 4.714), (920, 4.826), (940, 4.937), (960, 5.047),
(980, 5.155), (1000, 5.263), (1020, 5.369), (1040, 5.475), (1060, 5.581),
(1080, 5.686), (1100, 5.79), (1120, 5.895), (1140, 5.999), (1160, 6.103),
(1180, 6.207), (1200, 6.311)
]
AD8497 = [
(-260, -0.785), (-240, -0.773), (-220, -0.751), (-200, -0.718),
(-180, -0.676), (-160, -0.626), (-140, -0.568), (-120, -0.503),
(-100, -0.432), (-80, -0.354), (-60, -0.271), (-40, -0.184),
(-20, -0.092), (0, 0.003), (20, 0.101), (25, 0.126), (40, 0.2),
(60, 0.301), (80, 0.403), (100, 0.505), (120, 0.605), (140, 0.705),
(160, 0.804), (180, 0.902), (200, 0.999), (220, 1.097), (240, 1.196),
(260, 1.296), (280, 1.396), (300, 1.498), (320, 1.599), (340, 1.701),
(360, 1.804), (380, 1.907), (400, 2.01), (420, 2.114), (440, 2.218),
(460, 2.322), (480, 2.426), (500, 2.53), (520, 2.634), (540, 2.739),
(560, 2.843), (580, 2.948), (600, 3.052), (620, 3.156), (640, 3.259),
(660, 3.363), (680, 3.466), (700, 3.569), (720, 3.671), (740, 3.773),
(760, 3.874), (780, 3.976), (800, 4.076), (820, 4.176), (840, 4.276),
(860, 4.375), (880, 4.474), (900, 4.572), (920, 4.67), (940, 4.767),
(960, 4.863), (980, 4.96), (1000, 5.055), (1020, 5.151), (1040, 5.245),
(1060, 5.339), (1080, 5.433), (1100, 5.526), (1120, 5.618), (1140, 5.71),
(1160, 5.801), (1180, 5.891), (1200, 5.981), (1220, 6.07), (1240, 6.158),
(1260, 6.246), (1280, 6.332), (1300, 6.418), (1320, 6.503), (1340, 6.588),
(1360, 6.671), (1380, 6.754)
]
def calc_pt100(base=100.):
# Calc PT100/PT1000 resistances using Callendar-Van Dusen formula
A, B = (3.9083e-3, -5.775e-7)
return [(float(t), base * (1. + A*t + B*t*t)) for t in range(0, 500, 10)]
def calc_ina826_pt100():
# Standard circuit is 4400ohm pullup with 10x gain to 5V
return [(t, 10. * 5. * r / (4400. + r)) for t, r in calc_pt100()]
DefaultVoltageSensors = [
("AD595", AD595), ("AD597", AD597), ("AD8494", AD8494), ("AD8495", AD8495),
("AD8496", AD8496), ("AD8497", AD8497),
("PT100 INA826", calc_ina826_pt100())
]
DefaultResistanceSensors = [
("PT1000", calc_pt100(1000.))
]
def load_config(config):
# Register default sensors
pheaters = config.get_printer().load_object(config, "heaters")
for sensor_type, params in DefaultVoltageSensors:
func = (lambda config, params=params:
PrinterADCtoTemperature(config, LinearVoltage(config, params)))
pheaters.add_sensor_factory(sensor_type, func)
for sensor_type, params in DefaultResistanceSensors:
func = (lambda config, params=params:
PrinterADCtoTemperature(config,
LinearResistance(config, params)))
pheaters.add_sensor_factory(sensor_type, func)
def load_config_prefix(config):
if config.get("resistance1", None) is None:
custom_sensor = CustomLinearVoltage(config)
else:
custom_sensor = CustomLinearResistance(config)
pheaters = config.get_printer().load_object(config, "heaters")
pheaters.add_sensor_factory(custom_sensor.name, custom_sensor.create)

Binary file not shown.

444
klippy/extras/adxl345.py Normal file
View File

@@ -0,0 +1,444 @@
# Support for reading acceleration data from an adxl345 chip
#
# Copyright (C) 2020-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, time, collections, threading, multiprocessing, os
from . import bus, motion_report
# ADXL345 registers
REG_DEVID = 0x00
REG_BW_RATE = 0x2C
REG_POWER_CTL = 0x2D
REG_DATA_FORMAT = 0x31
REG_FIFO_CTL = 0x38
REG_MOD_READ = 0x80
REG_MOD_MULTI = 0x40
QUERY_RATES = {
25: 0x8, 50: 0x9, 100: 0xa, 200: 0xb, 400: 0xc,
800: 0xd, 1600: 0xe, 3200: 0xf,
}
ADXL345_DEV_ID = 0xe5
SET_FIFO_CTL = 0x90
FREEFALL_ACCEL = 9.80665 * 1000.
SCALE = 0.0039 * FREEFALL_ACCEL # 3.9mg/LSB * Earth gravity in mm/s**2
Accel_Measurement = collections.namedtuple(
'Accel_Measurement', ('time', 'accel_x', 'accel_y', 'accel_z'))
# Helper class to obtain measurements
class AccelQueryHelper:
def __init__(self, printer, cconn):
self.printer = printer
self.cconn = cconn
print_time = printer.lookup_object('toolhead').get_last_move_time()
self.request_start_time = self.request_end_time = print_time
self.samples = self.raw_samples = []
def finish_measurements(self):
toolhead = self.printer.lookup_object('toolhead')
self.request_end_time = toolhead.get_last_move_time()
toolhead.wait_moves()
self.cconn.finalize()
def _get_raw_samples(self):
raw_samples = self.cconn.get_messages()
if raw_samples:
self.raw_samples = raw_samples
return self.raw_samples
def has_valid_samples(self):
raw_samples = self._get_raw_samples()
for msg in raw_samples:
data = msg['params']['data']
first_sample_time = data[0][0]
last_sample_time = data[-1][0]
if (first_sample_time > self.request_end_time
or last_sample_time < self.request_start_time):
continue
# The time intervals [first_sample_time, last_sample_time]
# and [request_start_time, request_end_time] have non-zero
# intersection. It is still theoretically possible that none
# of the samples from raw_samples fall into the time interval
# [request_start_time, request_end_time] if it is too narrow
# or on very heavy data losses. In practice, that interval
# is at least 1 second, so this possibility is negligible.
return True
return False
def get_samples(self):
raw_samples = self._get_raw_samples()
if not raw_samples:
return self.samples
total = sum([len(m['params']['data']) for m in raw_samples])
count = 0
self.samples = samples = [None] * total
for msg in raw_samples:
for samp_time, x, y, z in msg['params']['data']:
if samp_time < self.request_start_time:
continue
if samp_time > self.request_end_time:
break
samples[count] = Accel_Measurement(samp_time, x, y, z)
count += 1
del samples[count:]
return self.samples
def write_to_file(self, filename):
def write_impl():
try:
# Try to re-nice writing process
os.nice(20)
except:
pass
f = open(filename, "w")
f.write("#time,accel_x,accel_y,accel_z\n")
samples = self.samples or self.get_samples()
for t, accel_x, accel_y, accel_z in samples:
f.write("%.6f,%.6f,%.6f,%.6f\n" % (
t, accel_x, accel_y, accel_z))
f.close()
write_proc = multiprocessing.Process(target=write_impl)
write_proc.daemon = True
write_proc.start()
# Helper class for G-Code commands
class AccelCommandHelper:
def __init__(self, config, chip):
self.printer = config.get_printer()
self.chip = chip
self.bg_client = None
name_parts = config.get_name().split()
self.base_name = name_parts[0]
self.name = name_parts[-1]
self.register_commands(self.name)
if len(name_parts) == 1:
if self.name == "adxl345" or not config.has_section("adxl345"):
self.register_commands(None)
def register_commands(self, name):
# Register commands
gcode = self.printer.lookup_object('gcode')
gcode.register_mux_command("ACCELEROMETER_MEASURE", "CHIP", name,
self.cmd_ACCELEROMETER_MEASURE,
desc=self.cmd_ACCELEROMETER_MEASURE_help)
gcode.register_mux_command("ACCELEROMETER_QUERY", "CHIP", name,
self.cmd_ACCELEROMETER_QUERY,
desc=self.cmd_ACCELEROMETER_QUERY_help)
gcode.register_mux_command("ACCELEROMETER_DEBUG_READ", "CHIP", name,
self.cmd_ACCELEROMETER_DEBUG_READ,
desc=self.cmd_ACCELEROMETER_DEBUG_READ_help)
gcode.register_mux_command("ACCELEROMETER_DEBUG_WRITE", "CHIP", name,
self.cmd_ACCELEROMETER_DEBUG_WRITE,
desc=self.cmd_ACCELEROMETER_DEBUG_WRITE_help)
cmd_ACCELEROMETER_MEASURE_help = "Start/stop accelerometer"
def cmd_ACCELEROMETER_MEASURE(self, gcmd):
if self.bg_client is None:
# Start measurements
self.bg_client = self.chip.start_internal_client()
gcmd.respond_info("accelerometer measurements started")
return
# End measurements
name = gcmd.get("NAME", time.strftime("%Y%m%d_%H%M%S"))
if not name.replace('-', '').replace('_', '').isalnum():
raise gcmd.error("Invalid NAME parameter")
bg_client = self.bg_client
self.bg_client = None
bg_client.finish_measurements()
# Write data to file
if self.base_name == self.name:
filename = "/tmp/%s-%s.csv" % (self.base_name, name)
else:
filename = "/tmp/%s-%s-%s.csv" % (self.base_name, self.name, name)
bg_client.write_to_file(filename)
gcmd.respond_info("Writing raw accelerometer data to %s file"
% (filename,))
cmd_ACCELEROMETER_QUERY_help = "Query accelerometer for the current values"
def cmd_ACCELEROMETER_QUERY(self, gcmd):
aclient = self.chip.start_internal_client()
self.printer.lookup_object('toolhead').dwell(1.)
aclient.finish_measurements()
values = aclient.get_samples()
if not values:
raise gcmd.error("No accelerometer measurements found")
_, accel_x, accel_y, accel_z = values[-1]
gcmd.respond_info("accelerometer values (x, y, z): %.6f, %.6f, %.6f"
% (accel_x, accel_y, accel_z))
cmd_ACCELEROMETER_DEBUG_READ_help = "Query register (for debugging)"
def cmd_ACCELEROMETER_DEBUG_READ(self, gcmd):
reg = gcmd.get("REG", minval=0, maxval=126, parser=lambda x: int(x, 0))
val = self.chip.read_reg(reg)
gcmd.respond_info("Accelerometer REG[0x%x] = 0x%x" % (reg, val))
cmd_ACCELEROMETER_DEBUG_WRITE_help = "Set register (for debugging)"
def cmd_ACCELEROMETER_DEBUG_WRITE(self, gcmd):
reg = gcmd.get("REG", minval=0, maxval=126, parser=lambda x: int(x, 0))
val = gcmd.get("VAL", minval=0, maxval=255, parser=lambda x: int(x, 0))
self.chip.set_reg(reg, val)
# Helper class for chip clock synchronization via linear regression
class ClockSyncRegression:
def __init__(self, mcu, chip_clock_smooth, decay = 1. / 20.):
self.mcu = mcu
self.chip_clock_smooth = chip_clock_smooth
self.decay = decay
self.last_chip_clock = self.last_exp_mcu_clock = 0.
self.mcu_clock_avg = self.mcu_clock_variance = 0.
self.chip_clock_avg = self.chip_clock_covariance = 0.
def reset(self, mcu_clock, chip_clock):
self.mcu_clock_avg = self.last_mcu_clock = mcu_clock
self.chip_clock_avg = chip_clock
self.mcu_clock_variance = self.chip_clock_covariance = 0.
self.last_chip_clock = self.last_exp_mcu_clock = 0.
def update(self, mcu_clock, chip_clock):
# Update linear regression
decay = self.decay
diff_mcu_clock = mcu_clock - self.mcu_clock_avg
self.mcu_clock_avg += decay * diff_mcu_clock
self.mcu_clock_variance = (1. - decay) * (
self.mcu_clock_variance + diff_mcu_clock**2 * decay)
diff_chip_clock = chip_clock - self.chip_clock_avg
self.chip_clock_avg += decay * diff_chip_clock
self.chip_clock_covariance = (1. - decay) * (
self.chip_clock_covariance + diff_mcu_clock*diff_chip_clock*decay)
def set_last_chip_clock(self, chip_clock):
base_mcu, base_chip, inv_cfreq = self.get_clock_translation()
self.last_chip_clock = chip_clock
self.last_exp_mcu_clock = base_mcu + (chip_clock-base_chip) * inv_cfreq
def get_clock_translation(self):
inv_chip_freq = self.mcu_clock_variance / self.chip_clock_covariance
if not self.last_chip_clock:
return self.mcu_clock_avg, self.chip_clock_avg, inv_chip_freq
# Find mcu clock associated with future chip_clock
s_chip_clock = self.last_chip_clock + self.chip_clock_smooth
scdiff = s_chip_clock - self.chip_clock_avg
s_mcu_clock = self.mcu_clock_avg + scdiff * inv_chip_freq
# Calculate frequency to converge at future point
mdiff = s_mcu_clock - self.last_exp_mcu_clock
s_inv_chip_freq = mdiff / self.chip_clock_smooth
return self.last_exp_mcu_clock, self.last_chip_clock, s_inv_chip_freq
def get_time_translation(self):
base_mcu, base_chip, inv_cfreq = self.get_clock_translation()
clock_to_print_time = self.mcu.clock_to_print_time
base_time = clock_to_print_time(base_mcu)
inv_freq = clock_to_print_time(base_mcu + inv_cfreq) - base_time
return base_time, base_chip, inv_freq
MIN_MSG_TIME = 0.100
BYTES_PER_SAMPLE = 5
SAMPLES_PER_BLOCK = 10
# Printer class that controls ADXL345 chip
class ADXL345:
def __init__(self, config):
self.printer = config.get_printer()
AccelCommandHelper(config, self)
self.query_rate = 0
am = {'x': (0, SCALE), 'y': (1, SCALE), 'z': (2, SCALE),
'-x': (0, -SCALE), '-y': (1, -SCALE), '-z': (2, -SCALE)}
axes_map = config.getlist('axes_map', ('x','y','z'), count=3)
if any([a not in am for a in axes_map]):
raise config.error("Invalid adxl345 axes_map parameter")
self.axes_map = [am[a.strip()] for a in axes_map]
self.data_rate = config.getint('rate', 3200)
if self.data_rate not in QUERY_RATES:
raise config.error("Invalid rate parameter: %d" % (self.data_rate,))
# Measurement storage (accessed from background thread)
self.lock = threading.Lock()
self.raw_samples = []
# Setup mcu sensor_adxl345 bulk query code
self.spi = bus.MCU_SPI_from_config(config, 3, default_speed=5000000)
self.mcu = mcu = self.spi.get_mcu()
self.oid = oid = mcu.create_oid()
self.query_adxl345_cmd = self.query_adxl345_end_cmd = None
self.query_adxl345_status_cmd = None
mcu.add_config_cmd("config_adxl345 oid=%d spi_oid=%d"
% (oid, self.spi.get_oid()))
mcu.add_config_cmd("query_adxl345 oid=%d clock=0 rest_ticks=0"
% (oid,), on_restart=True)
mcu.register_config_callback(self._build_config)
mcu.register_response(self._handle_adxl345_data, "adxl345_data", oid)
# Clock tracking
self.last_sequence = self.max_query_duration = 0
self.last_limit_count = self.last_error_count = 0
self.clock_sync = ClockSyncRegression(self.mcu, 640)
# API server endpoints
self.api_dump = motion_report.APIDumpHelper(
self.printer, self._api_update, self._api_startstop, 0.100)
self.name = config.get_name().split()[-1]
wh = self.printer.lookup_object('webhooks')
wh.register_mux_endpoint("adxl345/dump_adxl345", "sensor", self.name,
self._handle_dump_adxl345)
def _build_config(self):
cmdqueue = self.spi.get_command_queue()
self.query_adxl345_cmd = self.mcu.lookup_command(
"query_adxl345 oid=%c clock=%u rest_ticks=%u", cq=cmdqueue)
self.query_adxl345_end_cmd = self.mcu.lookup_query_command(
"query_adxl345 oid=%c clock=%u rest_ticks=%u",
"adxl345_status oid=%c clock=%u query_ticks=%u next_sequence=%hu"
" buffered=%c fifo=%c limit_count=%hu", oid=self.oid, cq=cmdqueue)
self.query_adxl345_status_cmd = self.mcu.lookup_query_command(
"query_adxl345_status oid=%c",
"adxl345_status oid=%c clock=%u query_ticks=%u next_sequence=%hu"
" buffered=%c fifo=%c limit_count=%hu", oid=self.oid, cq=cmdqueue)
def read_reg(self, reg):
params = self.spi.spi_transfer([reg | REG_MOD_READ, 0x00])
response = bytearray(params['response'])
return response[1]
def set_reg(self, reg, val, minclock=0):
self.spi.spi_send([reg, val & 0xFF], minclock=minclock)
stored_val = self.read_reg(reg)
if stored_val != val:
raise self.printer.command_error(
"Failed to set ADXL345 register [0x%x] to 0x%x: got 0x%x. "
"This is generally indicative of connection problems "
"(e.g. faulty wiring) or a faulty adxl345 chip." % (
reg, val, stored_val))
# Measurement collection
def is_measuring(self):
return self.query_rate > 0
def _handle_adxl345_data(self, params):
with self.lock:
self.raw_samples.append(params)
def _extract_samples(self, raw_samples):
# Load variables to optimize inner loop below
(x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = self.axes_map
last_sequence = self.last_sequence
time_base, chip_base, inv_freq = self.clock_sync.get_time_translation()
# Process every message in raw_samples
count = seq = 0
samples = [None] * (len(raw_samples) * SAMPLES_PER_BLOCK)
for params in raw_samples:
seq_diff = (last_sequence - params['sequence']) & 0xffff
seq_diff -= (seq_diff & 0x8000) << 1
seq = last_sequence - seq_diff
d = bytearray(params['data'])
msg_cdiff = seq * SAMPLES_PER_BLOCK - chip_base
for i in range(len(d) // BYTES_PER_SAMPLE):
d_xyz = d[i*BYTES_PER_SAMPLE:(i+1)*BYTES_PER_SAMPLE]
xlow, ylow, zlow, xzhigh, yzhigh = d_xyz
if yzhigh & 0x80:
self.last_error_count += 1
continue
rx = (xlow | ((xzhigh & 0x1f) << 8)) - ((xzhigh & 0x10) << 9)
ry = (ylow | ((yzhigh & 0x1f) << 8)) - ((yzhigh & 0x10) << 9)
rz = ((zlow | ((xzhigh & 0xe0) << 3) | ((yzhigh & 0xe0) << 6))
- ((yzhigh & 0x40) << 7))
raw_xyz = (rx, ry, rz)
x = round(raw_xyz[x_pos] * x_scale, 6)
y = round(raw_xyz[y_pos] * y_scale, 6)
z = round(raw_xyz[z_pos] * z_scale, 6)
ptime = round(time_base + (msg_cdiff + i) * inv_freq, 6)
samples[count] = (ptime, x, y, z)
count += 1
self.clock_sync.set_last_chip_clock(seq * SAMPLES_PER_BLOCK + i)
del samples[count:]
return samples
def _update_clock(self, minclock=0):
# Query current state
for retry in range(5):
params = self.query_adxl345_status_cmd.send([self.oid],
minclock=minclock)
fifo = params['fifo'] & 0x7f
if fifo <= 32:
break
else:
raise self.printer.command_error("Unable to query adxl345 fifo")
mcu_clock = self.mcu.clock32_to_clock64(params['clock'])
sequence = (self.last_sequence & ~0xffff) | params['next_sequence']
if sequence < self.last_sequence:
sequence += 0x10000
self.last_sequence = sequence
buffered = params['buffered']
limit_count = (self.last_limit_count & ~0xffff) | params['limit_count']
if limit_count < self.last_limit_count:
limit_count += 0x10000
self.last_limit_count = limit_count
duration = params['query_ticks']
if duration > self.max_query_duration:
# Skip measurement as a high query time could skew clock tracking
self.max_query_duration = max(2 * self.max_query_duration,
self.mcu.seconds_to_clock(.000005))
return
self.max_query_duration = 2 * duration
msg_count = (sequence * SAMPLES_PER_BLOCK
+ buffered // BYTES_PER_SAMPLE + fifo)
# The "chip clock" is the message counter plus .5 for average
# inaccuracy of query responses and plus .5 for assumed offset
# of adxl345 hw processing time.
chip_clock = msg_count + 1
self.clock_sync.update(mcu_clock + duration // 2, chip_clock)
def _start_measurements(self):
if self.is_measuring():
return
# In case of miswiring, testing ADXL345 device ID prevents treating
# noise or wrong signal as a correctly initialized device
dev_id = self.read_reg(REG_DEVID)
if dev_id != ADXL345_DEV_ID:
raise self.printer.command_error(
"Invalid adxl345 id (got %x vs %x).\n"
"This is generally indicative of connection problems\n"
"(e.g. faulty wiring) or a faulty adxl345 chip."
% (dev_id, ADXL345_DEV_ID))
# Setup chip in requested query rate
self.set_reg(REG_POWER_CTL, 0x00)
self.set_reg(REG_DATA_FORMAT, 0x0B)
self.set_reg(REG_FIFO_CTL, 0x00)
self.set_reg(REG_BW_RATE, QUERY_RATES[self.data_rate])
self.set_reg(REG_FIFO_CTL, SET_FIFO_CTL)
# Setup samples
with self.lock:
self.raw_samples = []
# Start bulk reading
systime = self.printer.get_reactor().monotonic()
print_time = self.mcu.estimated_print_time(systime) + MIN_MSG_TIME
reqclock = self.mcu.print_time_to_clock(print_time)
rest_ticks = self.mcu.seconds_to_clock(4. / self.data_rate)
self.query_rate = self.data_rate
self.query_adxl345_cmd.send([self.oid, reqclock, rest_ticks],
reqclock=reqclock)
logging.info("ADXL345 starting '%s' measurements", self.name)
# Initialize clock tracking
self.last_sequence = 0
self.last_limit_count = self.last_error_count = 0
self.clock_sync.reset(reqclock, 0)
self.max_query_duration = 1 << 31
self._update_clock(minclock=reqclock)
self.max_query_duration = 1 << 31
def _finish_measurements(self):
if not self.is_measuring():
return
# Halt bulk reading
params = self.query_adxl345_end_cmd.send([self.oid, 0, 0])
self.query_rate = 0
with self.lock:
self.raw_samples = []
logging.info("ADXL345 finished '%s' measurements", self.name)
# API interface
def _api_update(self, eventtime):
self._update_clock()
with self.lock:
raw_samples = self.raw_samples
self.raw_samples = []
if not raw_samples:
return {}
samples = self._extract_samples(raw_samples)
if not samples:
return {}
return {'data': samples, 'errors': self.last_error_count,
'overflows': self.last_limit_count}
def _api_startstop(self, is_start):
if is_start:
self._start_measurements()
else:
self._finish_measurements()
def _handle_dump_adxl345(self, web_request):
self.api_dump.add_client(web_request)
hdr = ('time', 'x_acceleration', 'y_acceleration', 'z_acceleration')
web_request.send({'header': hdr})
def start_internal_client(self):
cconn = self.api_dump.add_internal_client()
return AccelQueryHelper(self.printer, cconn)
def load_config(config):
return ADXL345(config)
def load_config_prefix(config):
return ADXL345(config)

BIN
klippy/extras/adxl345.pyc Normal file

Binary file not shown.

578
klippy/extras/angle.py Normal file
View File

@@ -0,0 +1,578 @@
# Support for reading SPI magnetic angle sensors
#
# Copyright (C) 2021,2022 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math, threading
from . import bus, motion_report
MIN_MSG_TIME = 0.100
TCODE_ERROR = 0xff
TRINAMIC_DRIVERS = ["tmc2130", "tmc2208", "tmc2209", "tmc2240", "tmc2660", "tmc5160"]
CALIBRATION_BITS = 6 # 64 entries
ANGLE_BITS = 16 # angles range from 0..65535
class AngleCalibration:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name()
self.stepper_name = config.get('stepper', None)
if self.stepper_name is None:
# No calibration
return
try:
import numpy
except:
raise config.error("Angle calibration requires numpy module")
sconfig = config.getsection(self.stepper_name)
sconfig.getint('microsteps', note_valid=False)
self.tmc_module = self.mcu_stepper = None
# Current calibration data
self.mcu_pos_offset = None
self.angle_phase_offset = 0.
self.calibration_reversed = False
self.calibration = []
cal = config.get('calibrate', None)
if cal is not None:
data = [d.strip() for d in cal.split(',')]
angles = [float(d) for d in data if d]
self.load_calibration(angles)
# Register commands
self.printer.register_event_handler("stepper:sync_mcu_position",
self.handle_sync_mcu_pos)
self.printer.register_event_handler("klippy:connect", self.connect)
cname = self.name.split()[-1]
gcode = self.printer.lookup_object('gcode')
gcode.register_mux_command("ANGLE_CALIBRATE", "CHIP",
cname, self.cmd_ANGLE_CALIBRATE,
desc=self.cmd_ANGLE_CALIBRATE_help)
def handle_sync_mcu_pos(self, mcu_stepper):
if mcu_stepper.get_name() == self.stepper_name:
self.mcu_pos_offset = None
def calc_mcu_pos_offset(self, sample):
# Lookup phase information
mcu_phase_offset, phases = self.tmc_module.get_phase_offset()
if mcu_phase_offset is None:
return
# Find mcu position at time of sample
angle_time, angle_pos = sample
mcu_pos = self.mcu_stepper.get_past_mcu_position(angle_time)
# Convert angle_pos to mcu_pos units
microsteps, full_steps = self.get_microsteps()
angle_to_mcu_pos = full_steps * microsteps / float(1<<ANGLE_BITS)
angle_mpos = angle_pos * angle_to_mcu_pos
# Calculate adjustment for stepper phases
phase_diff = ((angle_mpos + self.angle_phase_offset * angle_to_mcu_pos)
- (mcu_pos + mcu_phase_offset)) % phases
if phase_diff > phases//2:
phase_diff -= phases
# Store final offset
self.mcu_pos_offset = mcu_pos - (angle_mpos - phase_diff)
def apply_calibration(self, samples):
calibration = self.calibration
if not calibration:
return None
calibration_reversed = self.calibration_reversed
interp_bits = ANGLE_BITS - CALIBRATION_BITS
interp_mask = (1 << interp_bits) - 1
interp_round = 1 << (interp_bits - 1)
for i, (samp_time, angle) in enumerate(samples):
bucket = (angle & 0xffff) >> interp_bits
cal1 = calibration[bucket]
cal2 = calibration[bucket + 1]
adj = (angle & interp_mask) * (cal2 - cal1)
adj = cal1 + ((adj + interp_round) >> interp_bits)
angle_diff = (angle - adj) & 0xffff
angle_diff -= (angle_diff & 0x8000) << 1
new_angle = angle - angle_diff
if calibration_reversed:
new_angle = -new_angle
samples[i] = (samp_time, new_angle)
if self.mcu_pos_offset is None:
self.calc_mcu_pos_offset(samples[0])
if self.mcu_pos_offset is None:
return None
return self.mcu_stepper.mcu_to_commanded_position(self.mcu_pos_offset)
def load_calibration(self, angles):
# Calculate linear intepolation calibration buckets by solving
# linear equations
angle_max = 1 << ANGLE_BITS
calibration_count = 1 << CALIBRATION_BITS
bucket_size = angle_max // calibration_count
full_steps = len(angles)
nominal_step = float(angle_max) / full_steps
self.angle_phase_offset = (angles.index(min(angles)) & 3) * nominal_step
self.calibration_reversed = angles[-2] > angles[-1]
if self.calibration_reversed:
angles = list(reversed(angles))
first_step = angles.index(min(angles))
angles = angles[first_step:] + angles[:first_step]
import numpy
eqs = numpy.zeros((full_steps, calibration_count))
ans = numpy.zeros((full_steps,))
for step, angle in enumerate(angles):
int_angle = int(angle + .5) % angle_max
bucket = int(int_angle / bucket_size)
bucket_start = bucket * bucket_size
ang_diff = angle - bucket_start
ang_diff_per = ang_diff / bucket_size
eq = eqs[step]
eq[bucket] = 1. - ang_diff_per
eq[(bucket + 1) % calibration_count] = ang_diff_per
ans[step] = float(step * nominal_step)
if bucket + 1 >= calibration_count:
ans[step] -= ang_diff_per * angle_max
sol = numpy.linalg.lstsq(eqs, ans, rcond=None)[0]
isol = [int(s + .5) for s in sol]
self.calibration = isol + [isol[0] + angle_max]
def lookup_tmc(self):
for driver in TRINAMIC_DRIVERS:
driver_name = "%s %s" % (driver, self.stepper_name)
module = self.printer.lookup_object(driver_name, None)
if module is not None:
return module
raise self.printer.command_error("Unable to find TMC driver for %s"
% (self.stepper_name,))
def connect(self):
self.tmc_module = self.lookup_tmc()
fmove = self.printer.lookup_object('force_move')
self.mcu_stepper = fmove.lookup_stepper(self.stepper_name)
def get_microsteps(self):
configfile = self.printer.lookup_object('configfile')
sconfig = configfile.get_status(None)['settings']
stconfig = sconfig.get(self.stepper_name, {})
microsteps = stconfig['microsteps']
full_steps = stconfig['full_steps_per_rotation']
return microsteps, full_steps
def get_stepper_phase(self):
mcu_phase_offset, phases = self.tmc_module.get_phase_offset()
if mcu_phase_offset is None:
raise self.printer.command_error("Driver phase not known for %s"
% (self.stepper_name,))
mcu_pos = self.mcu_stepper.get_mcu_position()
return (mcu_pos + mcu_phase_offset) % phases
def do_calibration_moves(self):
move = self.printer.lookup_object('force_move').manual_move
# Start data collection
angle_sensor = self.printer.lookup_object(self.name)
cconn = angle_sensor.start_internal_client()
# Move stepper several turns (to allow internal sensor calibration)
microsteps, full_steps = self.get_microsteps()
mcu_stepper = self.mcu_stepper
step_dist = mcu_stepper.get_step_dist()
full_step_dist = step_dist * microsteps
rotation_dist = full_steps * full_step_dist
align_dist = step_dist * self.get_stepper_phase()
move_time = 0.010
move_speed = full_step_dist / move_time
move(mcu_stepper, -(rotation_dist+align_dist), move_speed)
move(mcu_stepper, 2. * rotation_dist, move_speed)
move(mcu_stepper, -2. * rotation_dist, move_speed)
move(mcu_stepper, .5 * rotation_dist - full_step_dist, move_speed)
# Move to each full step position
toolhead = self.printer.lookup_object('toolhead')
times = []
samp_dist = full_step_dist
for i in range(2 * full_steps):
move(mcu_stepper, samp_dist, move_speed)
start_query_time = toolhead.get_last_move_time() + 0.050
end_query_time = start_query_time + 0.050
times.append((start_query_time, end_query_time))
toolhead.dwell(0.150)
if i == full_steps-1:
# Reverse direction and test each full step again
move(mcu_stepper, .5 * rotation_dist, move_speed)
move(mcu_stepper, -.5 * rotation_dist + samp_dist, move_speed)
samp_dist = -samp_dist
move(mcu_stepper, .5*rotation_dist + align_dist, move_speed)
toolhead.wait_moves()
# Finish data collection
cconn.finalize()
msgs = cconn.get_messages()
# Correlate query responses
cal = {}
step = 0
for msg in msgs:
for query_time, pos in msg['params']['data']:
# Add to step tracking
while step < len(times) and query_time > times[step][1]:
step += 1
if step < len(times) and query_time >= times[step][0]:
cal.setdefault(step, []).append(pos)
if len(cal) != len(times):
raise self.printer.command_error(
"Failed calibration - incomplete sensor data")
fcal = { i: cal[i] for i in range(full_steps) }
rcal = { full_steps-i-1: cal[i+full_steps] for i in range(full_steps) }
return fcal, rcal
def calc_angles(self, meas):
total_count = total_variance = 0
angles = {}
for step, data in meas.items():
count = len(data)
angle_avg = float(sum(data)) / count
angles[step] = angle_avg
total_count += count
total_variance += sum([(d - angle_avg)**2 for d in data])
return angles, math.sqrt(total_variance / total_count), total_count
cmd_ANGLE_CALIBRATE_help = "Calibrate angle sensor to stepper motor"
def cmd_ANGLE_CALIBRATE(self, gcmd):
# Perform calibration movement and capture
old_calibration = self.calibration
self.calibration = []
try:
fcal, rcal = self.do_calibration_moves()
finally:
self.calibration = old_calibration
# Calculate each step position average and variance
microsteps, full_steps = self.get_microsteps()
fangles, fstd, ftotal = self.calc_angles(fcal)
rangles, rstd, rtotal = self.calc_angles(rcal)
if (len({a: i for i, a in fangles.items()}) != len(fangles)
or len({a: i for i, a in rangles.items()}) != len(rangles)):
raise self.printer.command_error(
"Failed calibration - sensor not updating for each step")
merged = { i: fcal[i] + rcal[i] for i in range(full_steps) }
angles, std, total = self.calc_angles(merged)
gcmd.respond_info("angle: stddev=%.3f (%.3f forward / %.3f reverse)"
" in %d queries" % (std, fstd, rstd, total))
# Order data with lowest/highest magnet position first
anglist = [angles[i] % 0xffff for i in range(full_steps)]
if angles[0] > angles[1]:
first_ang = max(anglist)
else:
first_ang = min(anglist)
first_phase = anglist.index(first_ang) & ~3
anglist = anglist[first_phase:] + anglist[:first_phase]
# Save results
cal_contents = []
for i, angle in enumerate(anglist):
if not i % 8:
cal_contents.append('\n')
cal_contents.append("%.1f" % (angle,))
cal_contents.append(',')
cal_contents.pop()
configfile = self.printer.lookup_object('configfile')
configfile.remove_section(self.name)
configfile.set(self.name, 'calibrate', ''.join(cal_contents))
class HelperA1333:
SPI_MODE = 3
SPI_SPEED = 10000000
def __init__(self, config, spi, oid):
self.spi = spi
self.is_tcode_absolute = False
self.last_temperature = None
def get_static_delay(self):
return .000001
def start(self):
# Setup for angle query
self.spi.spi_transfer([0x32, 0x00])
class HelperAS5047D:
SPI_MODE = 1
SPI_SPEED = int(1. / .000000350)
def __init__(self, config, spi, oid):
self.spi = spi
self.is_tcode_absolute = False
self.last_temperature = None
def get_static_delay(self):
return .000100
def start(self):
# Clear any errors from device
self.spi.spi_transfer([0xff, 0xfc]) # Read DIAAGC
self.spi.spi_transfer([0x40, 0x01]) # Read ERRFL
self.spi.spi_transfer([0xc0, 0x00]) # Read NOP
class HelperTLE5012B:
SPI_MODE = 1
SPI_SPEED = 4000000
def __init__(self, config, spi, oid):
self.printer = config.get_printer()
self.spi = spi
self.oid = oid
self.is_tcode_absolute = True
self.last_temperature = None
self.mcu = spi.get_mcu()
self.mcu.register_config_callback(self._build_config)
self.spi_angle_transfer_cmd = None
self.last_chip_mcu_clock = self.last_chip_clock = 0
self.chip_freq = 0.
name = config.get_name().split()[-1]
gcode = self.printer.lookup_object("gcode")
gcode.register_mux_command("ANGLE_DEBUG_READ", "CHIP", name,
self.cmd_ANGLE_DEBUG_READ,
desc=self.cmd_ANGLE_DEBUG_READ_help)
gcode.register_mux_command("ANGLE_DEBUG_WRITE", "CHIP", name,
self.cmd_ANGLE_DEBUG_WRITE,
desc=self.cmd_ANGLE_DEBUG_WRITE_help)
def _build_config(self):
cmdqueue = self.spi.get_command_queue()
self.spi_angle_transfer_cmd = self.mcu.lookup_query_command(
"spi_angle_transfer oid=%c data=%*s",
"spi_angle_transfer_response oid=%c clock=%u response=%*s",
oid=self.oid, cq=cmdqueue)
def get_tcode_params(self):
return self.last_chip_mcu_clock, self.last_chip_clock, self.chip_freq
def _calc_crc(self, data):
crc = 0xff
for d in data:
crc ^= d
for i in range(8):
if crc & 0x80:
crc = (crc << 1) ^ 0x1d
else:
crc <<= 1
return (~crc) & 0xff
def _send_spi(self, msg):
for retry in range(5):
if msg[0] & 0x04:
params = self.spi_angle_transfer_cmd.send([self.oid, msg])
else:
params = self.spi.spi_transfer(msg)
resp = bytearray(params['response'])
crc = self._calc_crc(bytearray(msg[:2]) + resp[2:-2])
if crc == resp[-1]:
return params
raise self.printer.command_error("Unable to query tle5012b chip")
def _read_reg(self, reg):
cw = 0x8000 | ((reg & 0x3f) << 4) | 0x01
if reg >= 0x05 and reg <= 0x11:
cw |= 0x5000
msg = [cw >> 8, cw & 0xff, 0, 0, 0, 0]
params = self._send_spi(msg)
resp = bytearray(params['response'])
return (resp[2] << 8) | resp[3]
def _write_reg(self, reg, val):
cw = ((reg & 0x3f) << 4) | 0x01
if reg >= 0x05 and reg <= 0x11:
cw |= 0x5000
msg = [cw >> 8, cw & 0xff, (val >> 8) & 0xff, val & 0xff, 0, 0]
for retry in range(5):
self._send_spi(msg)
rval = self._read_reg(reg)
if rval == val:
return
raise self.printer.command_error("Unable to write to tle5012b chip")
def _mask_reg(self, reg, off, on):
rval = self._read_reg(reg)
self._write_reg(reg, (rval & ~off) | on)
def _query_clock(self):
# Read frame counter (and normalize to a 16bit counter)
msg = [0x84, 0x42, 0, 0, 0, 0, 0, 0] # Read with latch, AREV and FSYNC
params = self._send_spi(msg)
resp = bytearray(params['response'])
mcu_clock = self.mcu.clock32_to_clock64(params['clock'])
chip_clock = ((resp[2] & 0x7e) << 9) | ((resp[4] & 0x3e) << 4)
# Calculate temperature
temper = resp[5] - ((resp[4] & 0x01) << 8)
self.last_temperature = (temper + 152) / 2.776
return mcu_clock, chip_clock
def update_clock(self):
mcu_clock, chip_clock = self._query_clock()
mdiff = mcu_clock - self.last_chip_mcu_clock
chip_mclock = self.last_chip_clock + int(mdiff * self.chip_freq + .5)
cdiff = (chip_mclock - chip_clock) & 0xffff
cdiff -= (cdiff & 0x8000) << 1
new_chip_clock = chip_mclock - cdiff
self.chip_freq = float(new_chip_clock - self.last_chip_clock) / mdiff
self.last_chip_clock = new_chip_clock
self.last_chip_mcu_clock = mcu_clock
def start(self):
# Clear any errors from device
self._read_reg(0x00) # Read STAT
# Initialize chip (so different chip variants work the same way)
self._mask_reg(0x06, 0xc003, 0x4000) # MOD1: 42.7us, IIF disable
self._mask_reg(0x08, 0x0007, 0x0001) # MOD2: Predict off, autocal=1
self._mask_reg(0x0e, 0x0003, 0x0000) # MOD4: IIF mode
# Setup starting clock values
mcu_clock, chip_clock = self._query_clock()
self.last_chip_clock = chip_clock
self.last_chip_mcu_clock = mcu_clock
self.chip_freq = float(1<<5) / self.mcu.seconds_to_clock(1. / 750000.)
self.update_clock()
cmd_ANGLE_DEBUG_READ_help = "Query low-level angle sensor register"
def cmd_ANGLE_DEBUG_READ(self, gcmd):
reg = gcmd.get("REG", minval=0, maxval=0x30, parser=lambda x: int(x, 0))
val = self._read_reg(reg)
gcmd.respond_info("ANGLE REG[0x%02x] = 0x%04x" % (reg, val))
cmd_ANGLE_DEBUG_WRITE_help = "Set low-level angle sensor register"
def cmd_ANGLE_DEBUG_WRITE(self, gcmd):
reg = gcmd.get("REG", minval=0, maxval=0x30, parser=lambda x: int(x, 0))
val = gcmd.get("VAL", minval=0, maxval=0xffff,
parser=lambda x: int(x, 0))
self._write_reg(reg, val)
SAMPLE_PERIOD = 0.000400
class Angle:
def __init__(self, config):
self.printer = config.get_printer()
self.sample_period = config.getfloat('sample_period', SAMPLE_PERIOD,
above=0.)
self.calibration = AngleCalibration(config)
# Measurement conversion
self.start_clock = self.time_shift = self.sample_ticks = 0
self.last_sequence = self.last_angle = 0
# Measurement storage (accessed from background thread)
self.lock = threading.Lock()
self.raw_samples = []
# Sensor type
sensors = { "a1333": HelperA1333, "as5047d": HelperAS5047D,
"tle5012b": HelperTLE5012B }
sensor_type = config.getchoice('sensor_type', {s: s for s in sensors})
sensor_class = sensors[sensor_type]
self.spi = bus.MCU_SPI_from_config(config, sensor_class.SPI_MODE,
default_speed=sensor_class.SPI_SPEED)
self.mcu = mcu = self.spi.get_mcu()
self.oid = oid = mcu.create_oid()
self.sensor_helper = sensor_class(config, self.spi, oid)
# Setup mcu sensor_spi_angle bulk query code
self.query_spi_angle_cmd = self.query_spi_angle_end_cmd = None
mcu.add_config_cmd(
"config_spi_angle oid=%d spi_oid=%d spi_angle_type=%s"
% (oid, self.spi.get_oid(), sensor_type))
mcu.add_config_cmd(
"query_spi_angle oid=%d clock=0 rest_ticks=0 time_shift=0"
% (oid,), on_restart=True)
mcu.register_config_callback(self._build_config)
mcu.register_response(self._handle_spi_angle_data,
"spi_angle_data", oid)
# API server endpoints
self.api_dump = motion_report.APIDumpHelper(
self.printer, self._api_update, self._api_startstop, 0.100)
self.name = config.get_name().split()[1]
wh = self.printer.lookup_object('webhooks')
wh.register_mux_endpoint("angle/dump_angle", "sensor", self.name,
self._handle_dump_angle)
def _build_config(self):
freq = self.mcu.seconds_to_clock(1.)
while float(TCODE_ERROR << self.time_shift) / freq < 0.002:
self.time_shift += 1
cmdqueue = self.spi.get_command_queue()
self.query_spi_angle_cmd = self.mcu.lookup_command(
"query_spi_angle oid=%c clock=%u rest_ticks=%u time_shift=%c",
cq=cmdqueue)
self.query_spi_angle_end_cmd = self.mcu.lookup_query_command(
"query_spi_angle oid=%c clock=%u rest_ticks=%u time_shift=%c",
"spi_angle_end oid=%c sequence=%hu", oid=self.oid, cq=cmdqueue)
def get_status(self, eventtime=None):
return {'temperature': self.sensor_helper.last_temperature}
# Measurement collection
def is_measuring(self):
return self.start_clock != 0
def _handle_spi_angle_data(self, params):
with self.lock:
self.raw_samples.append(params)
def _extract_samples(self, raw_samples):
# Load variables to optimize inner loop below
sample_ticks = self.sample_ticks
start_clock = self.start_clock
clock_to_print_time = self.mcu.clock_to_print_time
last_sequence = self.last_sequence
last_angle = self.last_angle
time_shift = 0
static_delay = 0.
last_chip_mcu_clock = last_chip_clock = chip_freq = inv_chip_freq = 0.
is_tcode_absolute = self.sensor_helper.is_tcode_absolute
if is_tcode_absolute:
tparams = self.sensor_helper.get_tcode_params()
last_chip_mcu_clock, last_chip_clock, chip_freq = tparams
inv_chip_freq = 1. / chip_freq
else:
time_shift = self.time_shift
static_delay = self.sensor_helper.get_static_delay()
# Process every message in raw_samples
count = error_count = 0
samples = [None] * (len(raw_samples) * 16)
for params in raw_samples:
seq = (last_sequence & ~0xffff) | params['sequence']
if seq < last_sequence:
seq += 0x10000
last_sequence = seq
d = bytearray(params['data'])
msg_mclock = start_clock + seq*16*sample_ticks
for i in range(len(d) // 3):
tcode = d[i*3]
if tcode == TCODE_ERROR:
error_count += 1
continue
raw_angle = d[i*3 + 1] | (d[i*3 + 2] << 8)
angle_diff = (last_angle - raw_angle) & 0xffff
angle_diff -= (angle_diff & 0x8000) << 1
last_angle -= angle_diff
mclock = msg_mclock + i*sample_ticks
if is_tcode_absolute:
# tcode is tle5012b frame counter
mdiff = mclock - last_chip_mcu_clock
chip_mclock = last_chip_clock + int(mdiff * chip_freq + .5)
cdiff = ((tcode << 10) - chip_mclock) & 0xffff
cdiff -= (cdiff & 0x8000) << 1
sclock = mclock + (cdiff - 0x800) * inv_chip_freq
else:
# tcode is mcu clock offset shifted by time_shift
sclock = mclock + (tcode<<time_shift)
ptime = round(clock_to_print_time(sclock) - static_delay, 6)
samples[count] = (ptime, last_angle)
count += 1
self.last_sequence = last_sequence
self.last_angle = last_angle
del samples[count:]
return samples, error_count
# API interface
def _api_update(self, eventtime):
if self.sensor_helper.is_tcode_absolute:
self.sensor_helper.update_clock()
with self.lock:
raw_samples = self.raw_samples
self.raw_samples = []
if not raw_samples:
return {}
samples, error_count = self._extract_samples(raw_samples)
if not samples:
return {}
offset = self.calibration.apply_calibration(samples)
return {'data': samples, 'errors': error_count,
'position_offset': offset}
def _start_measurements(self):
if self.is_measuring():
return
logging.info("Starting angle '%s' measurements", self.name)
self.sensor_helper.start()
# Start bulk reading
with self.lock:
self.raw_samples = []
self.last_sequence = 0
systime = self.printer.get_reactor().monotonic()
print_time = self.mcu.estimated_print_time(systime) + MIN_MSG_TIME
self.start_clock = reqclock = self.mcu.print_time_to_clock(print_time)
rest_ticks = self.mcu.seconds_to_clock(self.sample_period)
self.sample_ticks = rest_ticks
self.query_spi_angle_cmd.send([self.oid, reqclock, rest_ticks,
self.time_shift], reqclock=reqclock)
def _finish_measurements(self):
if not self.is_measuring():
return
# Halt bulk reading
params = self.query_spi_angle_end_cmd.send([self.oid, 0, 0, 0])
self.start_clock = 0
with self.lock:
self.raw_samples = []
self.sensor_helper.last_temperature = None
logging.info("Stopped angle '%s' measurements", self.name)
def _api_startstop(self, is_start):
if is_start:
self._start_measurements()
else:
self._finish_measurements()
def _handle_dump_angle(self, web_request):
self.api_dump.add_client(web_request)
hdr = ('time', 'angle')
web_request.send({'header': hdr})
def start_internal_client(self):
return self.api_dump.add_internal_client()
def load_config_prefix(config):
return Angle(config)

1243
klippy/extras/bed_mesh.py Normal file

File diff suppressed because it is too large Load Diff

BIN
klippy/extras/bed_mesh.pyc Normal file

Binary file not shown.

109
klippy/extras/bed_screws.py Normal file
View File

@@ -0,0 +1,109 @@
# Helper script to adjust bed screws
#
# Copyright (C) 2019-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class BedScrews:
def __init__(self, config):
self.printer = config.get_printer()
self.state = None
self.current_screw = 0
self.accepted_screws = 0
self.number_of_screws = 0
# Read config
screws = []
fine_adjust = []
for i in range(99):
prefix = "screw%d" % (i + 1,)
if config.get(prefix, None) is None:
break
screw_coord = config.getfloatlist(prefix, count=2)
screw_name = "screw at %.3f,%.3f" % screw_coord
screw_name = config.get(prefix + "_name", screw_name)
screws.append((screw_coord, screw_name))
pfa = prefix + "_fine_adjust"
if config.get(pfa, None) is not None:
fine_coord = config.getfloatlist(pfa, count=2)
fine_adjust.append((fine_coord, screw_name))
if len(screws) < 3:
raise config.error("bed_screws: Must have at least three screws")
self.number_of_screws = len(screws)
self.states = {'adjust': screws, 'fine': fine_adjust}
self.speed = config.getfloat('speed', 50., above=0.)
self.lift_speed = config.getfloat('probe_speed', 5., above=0.)
self.horizontal_move_z = config.getfloat('horizontal_move_z', 5.)
self.probe_z = config.getfloat('probe_height', 0.)
# Register command
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command("BED_SCREWS_ADJUST",
self.cmd_BED_SCREWS_ADJUST,
desc=self.cmd_BED_SCREWS_ADJUST_help)
def move(self, coord, speed):
self.printer.lookup_object('toolhead').manual_move(coord, speed)
def move_to_screw(self, state, screw):
# Move up, over, and then down
self.move((None, None, self.horizontal_move_z), self.lift_speed)
coord, name = self.states[state][screw]
self.move((coord[0], coord[1], self.horizontal_move_z), self.speed)
self.move((coord[0], coord[1], self.probe_z), self.lift_speed)
# Update state
self.state = state
self.current_screw = screw
# Register commands
self.gcode.respond_info(
"Adjust %s. Then run ACCEPT, ADJUSTED, or ABORT\n"
"Use ADJUSTED if a significant screw adjustment is made" % (name,))
self.gcode.register_command('ACCEPT', self.cmd_ACCEPT,
desc=self.cmd_ACCEPT_help)
self.gcode.register_command('ADJUSTED', self.cmd_ADJUSTED,
desc=self.cmd_ADJUSTED_help)
self.gcode.register_command('ABORT', self.cmd_ABORT,
desc=self.cmd_ABORT_help)
def unregister_commands(self):
self.gcode.register_command('ACCEPT', None)
self.gcode.register_command('ADJUSTED', None)
self.gcode.register_command('ABORT', None)
cmd_BED_SCREWS_ADJUST_help = "Tool to help adjust bed leveling screws"
def cmd_BED_SCREWS_ADJUST(self, gcmd):
if self.state is not None:
raise gcmd.error("Already in bed_screws helper; use ABORT to exit")
# reset accepted screws
self.accepted_screws = 0
self.move((None, None, self.horizontal_move_z), self.speed)
self.move_to_screw('adjust', 0)
cmd_ACCEPT_help = "Accept bed screw position"
def cmd_ACCEPT(self, gcmd):
self.unregister_commands()
self.accepted_screws = self.accepted_screws + 1
if self.current_screw + 1 < len(self.states[self.state]) \
and self.accepted_screws < self.number_of_screws:
# Continue with next screw
self.move_to_screw(self.state, self.current_screw + 1)
return
if self.accepted_screws < self.number_of_screws:
# Retry coarse adjustments
self.move_to_screw('adjust', 0)
return
if self.state == 'adjust' and self.states['fine']:
# Reset accepted screws for fine adjustment
self.accepted_screws = 0
# Perform fine screw adjustments
self.move_to_screw('fine', 0)
return
# Done
self.state = None
self.move((None, None, self.horizontal_move_z), self.lift_speed)
gcmd.respond_info("Bed screws tool completed successfully")
cmd_ADJUSTED_help = "Accept bed screw position after notable adjustment"
def cmd_ADJUSTED(self, gcmd):
self.unregister_commands()
self.accepted_screws = -1
self.cmd_ACCEPT(gcmd)
cmd_ABORT_help = "Abort bed screws tool"
def cmd_ABORT(self, gcmd):
self.unregister_commands()
self.state = None
def load_config(config):
return BedScrews(config)

99
klippy/extras/bed_tilt.py Normal file
View File

@@ -0,0 +1,99 @@
# Bed tilt compensation
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import mathutil
from . import probe
class BedTilt:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.x_adjust = config.getfloat('x_adjust', 0.)
self.y_adjust = config.getfloat('y_adjust', 0.)
self.z_adjust = config.getfloat('z_adjust', 0.)
if config.get('points', None) is not None:
BedTiltCalibrate(config, self)
self.toolhead = None
# Register move transform with g-code class
gcode_move = self.printer.load_object(config, 'gcode_move')
gcode_move.set_move_transform(self)
def handle_connect(self):
self.toolhead = self.printer.lookup_object('toolhead')
def get_position(self):
x, y, z, e = self.toolhead.get_position()
return [x, y, z - x*self.x_adjust - y*self.y_adjust - self.z_adjust, e]
def move(self, newpos, speed):
x, y, z, e = newpos
self.toolhead.move([x, y, z + x*self.x_adjust + y*self.y_adjust
+ self.z_adjust, e], speed)
def update_adjust(self, x_adjust, y_adjust, z_adjust):
self.x_adjust = x_adjust
self.y_adjust = y_adjust
self.z_adjust = z_adjust
gcode_move = self.printer.lookup_object('gcode_move')
gcode_move.reset_last_position()
configfile = self.printer.lookup_object('configfile')
configfile.set('bed_tilt', 'x_adjust', "%.6f" % (x_adjust,))
configfile.set('bed_tilt', 'y_adjust', "%.6f" % (y_adjust,))
configfile.set('bed_tilt', 'z_adjust', "%.6f" % (z_adjust,))
# Helper script to calibrate the bed tilt
class BedTiltCalibrate:
def __init__(self, config, bedtilt):
self.printer = config.get_printer()
self.bedtilt = bedtilt
self.probe_helper = probe.ProbePointsHelper(config, self.probe_finalize)
self.probe_helper.minimum_points(3)
# Register BED_TILT_CALIBRATE command
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command(
'BED_TILT_CALIBRATE', self.cmd_BED_TILT_CALIBRATE,
desc=self.cmd_BED_TILT_CALIBRATE_help)
cmd_BED_TILT_CALIBRATE_help = "Bed tilt calibration script"
def cmd_BED_TILT_CALIBRATE(self, gcmd):
self.probe_helper.start_probe(gcmd)
def probe_finalize(self, offsets, positions):
# Setup for coordinate descent analysis
z_offset = offsets[2]
logging.info("Calculating bed_tilt with: %s", positions)
params = { 'x_adjust': self.bedtilt.x_adjust,
'y_adjust': self.bedtilt.y_adjust,
'z_adjust': z_offset }
logging.info("Initial bed_tilt parameters: %s", params)
# Perform coordinate descent
def adjusted_height(pos, params):
x, y, z = pos
return (z - x*params['x_adjust'] - y*params['y_adjust']
- params['z_adjust'])
def errorfunc(params):
total_error = 0.
for pos in positions:
total_error += adjusted_height(pos, params)**2
return total_error
new_params = mathutil.coordinate_descent(
params.keys(), params, errorfunc)
# Update current bed_tilt calculations
x_adjust = new_params['x_adjust']
y_adjust = new_params['y_adjust']
z_adjust = (new_params['z_adjust'] - z_offset
- x_adjust * offsets[0] - y_adjust * offsets[1])
self.bedtilt.update_adjust(x_adjust, y_adjust, z_adjust)
# Log and report results
logging.info("Calculated bed_tilt parameters: %s", new_params)
for pos in positions:
logging.info("orig: %s new: %s", adjusted_height(pos, params),
adjusted_height(pos, new_params))
msg = "x_adjust: %.6f y_adjust: %.6f z_adjust: %.6f" % (
x_adjust, y_adjust, z_adjust)
self.printer.set_rollover_info("bed_tilt", "bed_tilt: %s" % (msg,))
self.gcode.respond_info(
"%s\nThe above parameters have been applied to the current\n"
"session. The SAVE_CONFIG command will update the printer\n"
"config file and restart the printer." % (msg,))
def load_config(config):
return BedTilt(config)

276
klippy/extras/bltouch.py Normal file
View File

@@ -0,0 +1,276 @@
# BLTouch support
#
# Copyright (C) 2018-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import probe
SIGNAL_PERIOD = 0.020
MIN_CMD_TIME = 5 * SIGNAL_PERIOD
TEST_TIME = 5 * 60.
RETRY_RESET_TIME = 1.
ENDSTOP_REST_TIME = .001
ENDSTOP_SAMPLE_TIME = .000015
ENDSTOP_SAMPLE_COUNT = 4
Commands = {
'pin_down': 0.000650, 'touch_mode': 0.001165,
'pin_up': 0.001475, 'self_test': 0.001780, 'reset': 0.002190,
'set_5V_output_mode' : 0.001988, 'set_OD_output_mode' : 0.002091,
'output_mode_store' : 0.001884,
}
# BLTouch "endstop" wrapper
class BLTouchEndstopWrapper:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.printer.register_event_handler('klippy:mcu_identify',
self.handle_mcu_identify)
self.position_endstop = config.getfloat('z_offset', minval=0.)
self.stow_on_each_sample = config.getboolean('stow_on_each_sample',
True)
self.probe_touch_mode = config.getboolean('probe_with_touch_mode',
False)
# Create a pwm object to handle the control pin
ppins = self.printer.lookup_object('pins')
self.mcu_pwm = ppins.setup_pin('pwm', config.get('control_pin'))
self.mcu_pwm.setup_max_duration(0.)
self.mcu_pwm.setup_cycle_time(SIGNAL_PERIOD)
# Command timing
self.next_cmd_time = self.action_end_time = 0.
self.finish_home_complete = self.wait_trigger_complete = None
# Create an "endstop" object to handle the sensor pin
pin = config.get('sensor_pin')
pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True)
mcu = pin_params['chip']
self.mcu_endstop = mcu.setup_pin('endstop', pin_params)
# output mode
omodes = {'5V': '5V', 'OD': 'OD', None: None}
self.output_mode = config.getchoice('set_output_mode', omodes, None)
# Setup for sensor test
self.next_test_time = 0.
self.pin_up_not_triggered = config.getboolean(
'pin_up_reports_not_triggered', True)
self.pin_up_touch_triggered = config.getboolean(
'pin_up_touch_mode_reports_triggered', True)
# Calculate pin move time
self.pin_move_time = config.getfloat('pin_move_time', 0.680, above=0.)
# Wrappers
self.get_mcu = self.mcu_endstop.get_mcu
self.add_stepper = self.mcu_endstop.add_stepper
self.get_steppers = self.mcu_endstop.get_steppers
self.home_wait = self.mcu_endstop.home_wait
self.query_endstop = self.mcu_endstop.query_endstop
# Register BLTOUCH_DEBUG command
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command("BLTOUCH_DEBUG", self.cmd_BLTOUCH_DEBUG,
desc=self.cmd_BLTOUCH_DEBUG_help)
self.gcode.register_command("BLTOUCH_STORE", self.cmd_BLTOUCH_STORE,
desc=self.cmd_BLTOUCH_STORE_help)
# multi probes state
self.multi = 'OFF'
def handle_mcu_identify(self):
kin = self.printer.lookup_object('toolhead').get_kinematics()
for stepper in kin.get_steppers():
if stepper.is_active_axis('z'):
self.add_stepper(stepper)
def handle_connect(self):
self.sync_mcu_print_time()
self.next_cmd_time += 0.200
self.set_output_mode(self.output_mode)
try:
self.raise_probe()
self.verify_raise_probe()
except self.printer.command_error as e:
logging.warning("BLTouch raise probe error: %s", str(e))
def sync_mcu_print_time(self):
curtime = self.printer.get_reactor().monotonic()
est_time = self.mcu_pwm.get_mcu().estimated_print_time(curtime)
self.next_cmd_time = max(self.next_cmd_time, est_time + MIN_CMD_TIME)
def sync_print_time(self):
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
if self.next_cmd_time > print_time:
toolhead.dwell(self.next_cmd_time - print_time)
else:
self.next_cmd_time = print_time
def send_cmd(self, cmd, duration=MIN_CMD_TIME):
# Translate duration to ticks to avoid any secondary mcu clock skew
mcu = self.mcu_pwm.get_mcu()
cmd_clock = mcu.print_time_to_clock(self.next_cmd_time)
pulse = int((duration - MIN_CMD_TIME) / SIGNAL_PERIOD) * SIGNAL_PERIOD
cmd_clock += mcu.seconds_to_clock(max(MIN_CMD_TIME, pulse))
end_time = mcu.clock_to_print_time(cmd_clock)
# Schedule command followed by PWM disable
self.mcu_pwm.set_pwm(self.next_cmd_time, Commands[cmd] / SIGNAL_PERIOD)
self.mcu_pwm.set_pwm(end_time, 0.)
# Update time tracking
self.action_end_time = self.next_cmd_time + duration
self.next_cmd_time = max(self.action_end_time, end_time + MIN_CMD_TIME)
def verify_state(self, triggered):
# Perform endstop check to verify bltouch reports desired state
self.mcu_endstop.home_start(self.action_end_time, ENDSTOP_SAMPLE_TIME,
ENDSTOP_SAMPLE_COUNT, ENDSTOP_REST_TIME,
triggered=triggered)
trigger_time = self.mcu_endstop.home_wait(self.action_end_time + 0.100)
return trigger_time > 0.
def raise_probe(self):
self.sync_mcu_print_time()
if not self.pin_up_not_triggered:
self.send_cmd('reset')
self.send_cmd('pin_up', duration=self.pin_move_time)
def verify_raise_probe(self):
if not self.pin_up_not_triggered:
# No way to verify raise attempt
return
for retry in range(3):
success = self.verify_state(False)
if success:
# The "probe raised" test completed successfully
break
if retry >= 2:
raise self.printer.command_error(
"BLTouch failed to raise probe")
msg = "Failed to verify BLTouch probe is raised; retrying."
self.gcode.respond_info(msg)
self.sync_mcu_print_time()
self.send_cmd('reset', duration=RETRY_RESET_TIME)
self.send_cmd('pin_up', duration=self.pin_move_time)
def lower_probe(self):
self.test_sensor()
self.sync_print_time()
self.send_cmd('pin_down', duration=self.pin_move_time)
if self.probe_touch_mode:
self.send_cmd('touch_mode')
def test_sensor(self):
if not self.pin_up_touch_triggered:
# Nothing to test
return
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
if print_time < self.next_test_time:
self.next_test_time = print_time + TEST_TIME
return
# Raise the bltouch probe and test if probe is raised
self.sync_print_time()
for retry in range(3):
self.send_cmd('pin_up', duration=self.pin_move_time)
self.send_cmd('touch_mode')
success = self.verify_state(True)
self.sync_print_time()
if success:
# The "bltouch connection" test completed successfully
self.next_test_time = print_time + TEST_TIME
return
msg = "BLTouch failed to verify sensor state"
if retry >= 2:
raise self.printer.command_error(msg)
self.gcode.respond_info(msg + '; retrying.')
self.send_cmd('reset', duration=RETRY_RESET_TIME)
def multi_probe_begin(self):
if self.stow_on_each_sample:
return
self.multi = 'FIRST'
def multi_probe_end(self):
if self.stow_on_each_sample:
return
self.sync_print_time()
self.raise_probe()
self.verify_raise_probe()
self.sync_print_time()
self.multi = 'OFF'
def probe_prepare(self, hmove):
if self.multi == 'OFF' or self.multi == 'FIRST':
self.lower_probe()
if self.multi == 'FIRST':
self.multi = 'ON'
self.sync_print_time()
def home_start(self, print_time, sample_time, sample_count, rest_time,
triggered=True):
rest_time = min(rest_time, ENDSTOP_REST_TIME)
self.finish_home_complete = self.mcu_endstop.home_start(
print_time, sample_time, sample_count, rest_time, triggered)
# Schedule wait_for_trigger callback
r = self.printer.get_reactor()
self.wait_trigger_complete = r.register_callback(self.wait_for_trigger)
return self.finish_home_complete
def wait_for_trigger(self, eventtime):
self.finish_home_complete.wait()
if self.multi == 'OFF':
self.raise_probe()
def probe_finish(self, hmove):
self.wait_trigger_complete.wait()
if self.multi == 'OFF':
self.verify_raise_probe()
self.sync_print_time()
if hmove.check_no_movement() is not None:
raise self.printer.command_error("BLTouch failed to deploy")
def get_position_endstop(self):
return self.position_endstop
def set_output_mode(self, mode):
# If this is inadvertently/purposely issued for a
# BLTOUCH pre V3.0 and clones:
# No reaction at all.
# BLTOUCH V3.0 and V3.1:
# This will set the mode.
if mode is None:
return
logging.info("BLTouch set output mode: %s", mode)
self.sync_mcu_print_time()
if mode == '5V':
self.send_cmd('set_5V_output_mode')
if mode == 'OD':
self.send_cmd('set_OD_output_mode')
def store_output_mode(self, mode):
# If this command is inadvertently/purposely issued for a
# BLTOUCH pre V3.0 and clones:
# No reaction at all to this sequence apart from a pin-down/pin-up
# BLTOUCH V3.0:
# This will set the mode (twice) and sadly, a pin-up is needed at
# the end, because of the pin-down
# BLTOUCH V3.1:
# This will set the mode and store it in the eeprom.
# The pin-up is not needed but does not hurt
logging.info("BLTouch store output mode: %s", mode)
self.sync_print_time()
self.send_cmd('pin_down')
if mode == '5V':
self.send_cmd('set_5V_output_mode')
else:
self.send_cmd('set_OD_output_mode')
self.send_cmd('output_mode_store')
if mode == '5V':
self.send_cmd('set_5V_output_mode')
else:
self.send_cmd('set_OD_output_mode')
self.send_cmd('pin_up')
cmd_BLTOUCH_DEBUG_help = "Send a command to the bltouch for debugging"
def cmd_BLTOUCH_DEBUG(self, gcmd):
cmd = gcmd.get('COMMAND', None)
if cmd is None or cmd not in Commands:
gcmd.respond_info("BLTouch commands: %s" % (
", ".join(sorted([c for c in Commands if c is not None]))))
return
gcmd.respond_info("Sending BLTOUCH_DEBUG COMMAND=%s" % (cmd,))
self.sync_print_time()
self.send_cmd(cmd, duration=self.pin_move_time)
self.sync_print_time()
cmd_BLTOUCH_STORE_help = "Store an output mode in the BLTouch EEPROM"
def cmd_BLTOUCH_STORE(self, gcmd):
cmd = gcmd.get('MODE', None)
if cmd is None or cmd not in ['5V', 'OD']:
gcmd.respond_info("BLTouch output modes: 5V, OD")
return
gcmd.respond_info("Storing BLTouch output mode: %s" % (cmd,))
self.sync_print_time()
self.store_output_mode(cmd)
self.sync_print_time()
def load_config(config):
blt = BLTouchEndstopWrapper(config)
config.get_printer().add_object('probe', probe.PrinterProbe(config, blt))
return blt

BIN
klippy/extras/bltouch.pyc Normal file

Binary file not shown.

480
klippy/extras/bme280.py Normal file
View File

@@ -0,0 +1,480 @@
# Support for i2c based temperature sensors
#
# Copyright (C) 2020 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import bus
REPORT_TIME = .8
BME280_CHIP_ADDR = 0x76
BME280_REGS = {
'RESET': 0xE0, 'CTRL_HUM': 0xF2,
'STATUS': 0xF3, 'CTRL_MEAS': 0xF4, 'CONFIG': 0xF5,
'PRESSURE_MSB': 0xF7, 'PRESSURE_LSB': 0xF8, 'PRESSURE_XLSB': 0xF9,
'TEMP_MSB': 0xFA, 'TEMP_LSB': 0xFB, 'TEMP_XLSB': 0xFC,
'HUM_MSB': 0xFD, 'HUM_LSB': 0xFE, 'CAL_1': 0x88, 'CAL_2': 0xE1
}
BME680_REGS = {
'RESET': 0xE0, 'CTRL_HUM': 0x72, 'CTRL_GAS_1': 0x71, 'CTRL_GAS_0': 0x70,
'GAS_WAIT_0': 0x64, 'RES_HEAT_0': 0x5A, 'IDAC_HEAT_0': 0x50,
'STATUS': 0x73, 'EAS_STATUS_0': 0x1D, 'CTRL_MEAS': 0x74, 'CONFIG': 0x75,
'GAS_R_LSB': 0x2B, 'GAS_R_MSB': 0x2A,
'PRESSURE_MSB': 0x1F, 'PRESSURE_LSB': 0x20, 'PRESSURE_XLSB': 0x21,
'TEMP_MSB': 0x22, 'TEMP_LSB': 0x23, 'TEMP_XLSB': 0x24,
'HUM_MSB': 0x25, 'HUM_LSB': 0x26, 'CAL_1': 0x88, 'CAL_2': 0xE1,
'RES_HEAT_VAL': 0x00, 'RES_HEAT_RANGE': 0x02, 'RANGE_SWITCHING_ERROR': 0x04
}
BME680_GAS_CONSTANTS = {
0: (1., 8000000.),
1: (1., 4000000.),
2: (1., 2000000.),
3: (1., 1000000.),
4: (1., 499500.4995),
5: (0.99, 248262.1648),
6: (1., 125000.),
7: (0.992, 63004.03226),
8: (1., 31281.28128),
9: (1., 15625.),
10: (0.998, 7812.5),
11: (0.995, 3906.25),
12: (1., 1953.125),
13: (0.99, 976.5625),
14: (1., 488.28125),
15: (1., 244.140625)
}
STATUS_MEASURING = 1 << 3
STATUS_IM_UPDATE = 1
MODE = 1
RUN_GAS = 1 << 4
NB_CONV_0 = 0
EAS_NEW_DATA = 1 << 7
GAS_DONE = 1 << 6
MEASURE_DONE = 1 << 5
RESET_CHIP_VALUE = 0xB6
BME_CHIPS = {
0x58: 'BMP280', 0x60: 'BME280', 0x61: 'BME680'
}
BME_CHIP_ID_REG = 0xD0
def get_twos_complement(val, bit_size):
if val & (1 << (bit_size - 1)):
val -= (1 << bit_size)
return val
def get_unsigned_short(bits):
return bits[1] << 8 | bits[0]
def get_signed_short(bits):
val = get_unsigned_short(bits)
return get_twos_complement(val, 16)
def get_signed_byte(bits):
return get_twos_complement(bits, 8)
class BME280:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.reactor = self.printer.get_reactor()
self.i2c = bus.MCU_I2C_from_config(
config, default_addr=BME280_CHIP_ADDR, default_speed=100000)
self.mcu = self.i2c.get_mcu()
self.iir_filter = config.getint('bme280_iir_filter', 1)
self.os_temp = config.getint('bme280_oversample_temp', 2)
self.os_hum = config.getint('bme280_oversample_hum', 2)
self.os_pres = config.getint('bme280_oversample_pressure', 2)
self.gas_heat_temp = config.getint('bme280_gas_target_temp', 320)
self.gas_heat_duration = config.getint('bme280_gas_heat_duration', 150)
logging.info("BMxx80: Oversampling: Temp %dx Humid %dx Pressure %dx" % (
pow(2, self.os_temp - 1), pow(2, self.os_hum - 1),
pow(2, self.os_pres - 1)))
logging.info("BMxx80: IIR: %dx" % (pow(2, self.iir_filter) - 1))
self.temp = self.pressure = self.humidity = self.gas = self.t_fine = 0.
self.min_temp = self.max_temp = self.range_switching_error = 0.
self.max_sample_time = None
self.dig = self.sample_timer = None
self.chip_type = 'BMP280'
self.chip_registers = BME280_REGS
self.printer.add_object("bme280 " + self.name, self)
if self.printer.get_start_args().get('debugoutput') is not None:
return
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
def handle_connect(self):
self._init_bmxx80()
self.reactor.update_timer(self.sample_timer, self.reactor.NOW)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
def setup_callback(self, cb):
self._callback = cb
def get_report_time_delta(self):
return REPORT_TIME
def _init_bmxx80(self):
def read_calibration_data_bmp280(calib_data_1):
dig = {}
dig['T1'] = get_unsigned_short(calib_data_1[0:2])
dig['T2'] = get_signed_short(calib_data_1[2:4])
dig['T3'] = get_signed_short(calib_data_1[4:6])
dig['P1'] = get_unsigned_short(calib_data_1[6:8])
dig['P2'] = get_signed_short(calib_data_1[8:10])
dig['P3'] = get_signed_short(calib_data_1[10:12])
dig['P4'] = get_signed_short(calib_data_1[12:14])
dig['P5'] = get_signed_short(calib_data_1[14:16])
dig['P6'] = get_signed_short(calib_data_1[16:18])
dig['P7'] = get_signed_short(calib_data_1[18:20])
dig['P8'] = get_signed_short(calib_data_1[20:22])
dig['P9'] = get_signed_short(calib_data_1[22:24])
return dig
def read_calibration_data_bme280(calib_data_1, calib_data_2):
dig = read_calibration_data_bmp280(calib_data_1)
dig['H1'] = calib_data_1[25] & 0xFF
dig['H2'] = get_signed_short(calib_data_2[0:2])
dig['H3'] = calib_data_2[2] & 0xFF
dig['H4'] = get_twos_complement(
(calib_data_2[3] << 4) | (calib_data_2[4] & 0x0F), 12)
dig['H5'] = get_twos_complement(
(calib_data_2[5] << 4) | ((calib_data_2[4] & 0xF0) >> 4), 12)
dig['H6'] = get_twos_complement(calib_data_2[6], 8)
return dig
def read_calibration_data_bme680(calib_data_1, calib_data_2):
dig = {}
dig['T1'] = get_unsigned_short(calib_data_2[8:10])
dig['T2'] = get_signed_short(calib_data_1[2:4])
dig['T3'] = get_signed_byte(calib_data_1[4])
dig['P1'] = get_unsigned_short(calib_data_1[6:8])
dig['P2'] = get_signed_short(calib_data_1[8:10])
dig['P3'] = calib_data_1[10]
dig['P4'] = get_signed_short(calib_data_1[12:14])
dig['P5'] = get_signed_short(calib_data_1[14:16])
dig['P6'] = get_signed_byte(calib_data_1[17])
dig['P7'] = get_signed_byte(calib_data_1[16])
dig['P8'] = get_signed_short(calib_data_1[20:22])
dig['P9'] = get_signed_short(calib_data_1[22:24])
dig['P10'] = calib_data_1[24]
dig['H1'] = get_twos_complement(
(calib_data_2[2] << 4) | (calib_data_2[1] & 0x0F), 12)
dig['H2'] = get_twos_complement(
(calib_data_2[0] << 4) | ((calib_data_2[1] & 0xF0) >> 4), 12)
dig['H3'] = get_signed_byte(calib_data_2[3])
dig['H4'] = get_signed_byte(calib_data_2[4])
dig['H5'] = get_signed_byte(calib_data_2[5])
dig['H6'] = calib_data_2[6]
dig['H7'] = get_signed_byte(calib_data_2[7])
dig['G1'] = get_signed_byte(calib_data_2[12])
dig['G2'] = get_signed_short(calib_data_2[10:12])
dig['G3'] = get_signed_byte(calib_data_2[13])
return dig
chip_id = self.read_id()
if chip_id not in BME_CHIPS.keys():
logging.info("bme280: Unknown Chip ID received %#x" % chip_id)
else:
self.chip_type = BME_CHIPS[chip_id]
logging.info("bme280: Found Chip %s at %#x" % (
self.chip_type, self.i2c.i2c_address))
# Reset chip
self.write_register('RESET', [RESET_CHIP_VALUE])
self.reactor.pause(self.reactor.monotonic() + .5)
# Make sure non-volatile memory has been copied to registers
status = self.read_register('STATUS', 1)[0]
while status & STATUS_IM_UPDATE:
self.reactor.pause(self.reactor.monotonic() + .01)
status = self.read_register('STATUS', 1)[0]
if self.chip_type == 'BME680':
self.max_sample_time = 0.5
self.sample_timer = self.reactor.register_timer(self._sample_bme680)
self.chip_registers = BME680_REGS
else:
self.max_sample_time = \
(1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575)
+ ((2.3 * self.os_hum) + .575)) / 1000
self.sample_timer = self.reactor.register_timer(self._sample_bme280)
self.chip_registers = BME280_REGS
if self.chip_type in ('BME680', 'BME280'):
self.write_register('CONFIG', (self.iir_filter & 0x07) << 2)
# Read out and calculate the trimming parameters
cal_1 = self.read_register('CAL_1', 26)
cal_2 = self.read_register('CAL_2', 16)
if self.chip_type == 'BME280':
self.dig = read_calibration_data_bme280(cal_1, cal_2)
elif self.chip_type == 'BMP280':
self.dig = read_calibration_data_bmp280(cal_1)
elif self.chip_type == 'BME680':
self.dig = read_calibration_data_bme680(cal_1, cal_2)
def _sample_bme280(self, eventtime):
# Enter forced mode
if self.chip_type == 'BME280':
self.write_register('CTRL_HUM', self.os_hum)
meas = self.os_temp << 5 | self.os_pres << 2 | MODE
self.write_register('CTRL_MEAS', meas)
try:
# wait until results are ready
status = self.read_register('STATUS', 1)[0]
while status & STATUS_MEASURING:
self.reactor.pause(
self.reactor.monotonic() + self.max_sample_time)
status = self.read_register('STATUS', 1)[0]
if self.chip_type == 'BME280':
data = self.read_register('PRESSURE_MSB', 8)
elif self.chip_type == 'BMP280':
data = self.read_register('PRESSURE_MSB', 6)
else:
return self.reactor.NEVER
except Exception:
logging.exception("BME280: Error reading data")
self.temp = self.pressure = self.humidity = .0
return self.reactor.NEVER
temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
self.temp = self._compensate_temp(temp_raw)
pressure_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
self.pressure = self._compensate_pressure_bme280(pressure_raw) / 100.
if self.chip_type == 'BME280':
humid_raw = (data[6] << 8) | data[7]
self.humidity = self._compensate_humidity_bme280(humid_raw)
if self.temp < self.min_temp or self.temp > self.max_temp:
self.printer.invoke_shutdown(
"BME280 temperature %0.1f outside range of %0.1f:%.01f"
% (self.temp, self.min_temp, self.max_temp))
measured_time = self.reactor.monotonic()
self._callback(self.mcu.estimated_print_time(measured_time), self.temp)
return measured_time + REPORT_TIME
def _sample_bme680(self, eventtime):
self.write_register('CTRL_HUM', self.os_hum & 0x07)
meas = self.os_temp << 5 | self.os_pres << 2
self.write_register('CTRL_MEAS', [meas])
gas_wait_0 = self._calculate_gas_heater_duration(self.gas_heat_duration)
self.write_register('GAS_WAIT_0', [gas_wait_0])
res_heat_0 = self._calculate_gas_heater_resistance(self.gas_heat_temp)
self.write_register('RES_HEAT_0', [res_heat_0])
gas_config = RUN_GAS | NB_CONV_0
self.write_register('CTRL_GAS_1', [gas_config])
def data_ready(stat):
new_data = (stat & EAS_NEW_DATA)
gas_done = not (stat & GAS_DONE)
meas_done = not (stat & MEASURE_DONE)
return new_data and gas_done and meas_done
# Enter forced mode
meas = meas | MODE
self.write_register('CTRL_MEAS', meas)
try:
# wait until results are ready
status = self.read_register('EAS_STATUS_0', 1)[0]
while not data_ready(status):
self.reactor.pause(
self.reactor.monotonic() + self.max_sample_time)
status = self.read_register('EAS_STATUS_0', 1)[0]
data = self.read_register('PRESSURE_MSB', 8)
gas_data = self.read_register('GAS_R_MSB', 2)
except Exception:
logging.exception("BME680: Error reading data")
self.temp = self.pressure = self.humidity = self.gas = .0
return self.reactor.NEVER
temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
if temp_raw != 0x80000:
self.temp = self._compensate_temp(temp_raw)
pressure_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
if pressure_raw != 0x80000:
self.pressure = self._compensate_pressure_bme680(
pressure_raw) / 100.
humid_raw = (data[6] << 8) | data[7]
self.humidity = self._compensate_humidity_bme680(humid_raw)
gas_valid = ((gas_data[1] & 0x20) == 0x20)
if gas_valid:
gas_heater_stable = ((gas_data[1] & 0x10) == 0x10)
if not gas_heater_stable:
logging.warning("BME680: Gas heater didn't reach target")
gas_raw = (gas_data[0] << 2) | ((gas_data[1] & 0xC0) >> 6)
gas_range = (gas_data[1] & 0x0F)
self.gas = self._compensate_gas(gas_raw, gas_range)
if self.temp < self.min_temp or self.temp > self.max_temp:
self.printer.invoke_shutdown(
"BME680 temperature %0.1f outside range of %0.1f:%.01f"
% (self.temp, self.min_temp, self.max_temp))
measured_time = self.reactor.monotonic()
self._callback(self.mcu.estimated_print_time(measured_time), self.temp)
return measured_time + REPORT_TIME * 4
def _compensate_temp(self, raw_temp):
dig = self.dig
var1 = ((raw_temp / 16384. - (dig['T1'] / 1024.)) * dig['T2'])
var2 = (
((raw_temp / 131072.) - (dig['T1'] / 8192.)) *
((raw_temp / 131072.) - (dig['T1'] / 8192.)) * dig['T3'])
self.t_fine = var1 + var2
return self.t_fine / 5120.0
def _compensate_pressure_bme280(self, raw_pressure):
dig = self.dig
t_fine = self.t_fine
var1 = t_fine / 2. - 64000.
var2 = var1 * var1 * dig['P6'] / 32768.
var2 = var2 + var1 * dig['P5'] * 2.
var2 = var2 / 4. + (dig['P4'] * 65536.)
var1 = (dig['P3'] * var1 * var1 / 524288. + dig['P2'] * var1) / 524288.
var1 = (1. + var1 / 32768.) * dig['P1']
if var1 == 0:
return 0.
else:
pressure = 1048576.0 - raw_pressure
pressure = ((pressure - var2 / 4096.) * 6250.) / var1
var1 = dig['P9'] * pressure * pressure / 2147483648.
var2 = pressure * dig['P8'] / 32768.
return pressure + (var1 + var2 + dig['P7']) / 16.
def _compensate_pressure_bme680(self, raw_pressure):
dig = self.dig
t_fine = self.t_fine
var1 = t_fine / 2. - 64000.
var2 = var1 * var1 * dig['P6'] / 131072.
var2 = var2 + var1 * dig['P5'] * 2.
var2 = var2 / 4. + (dig['P4'] * 65536.)
var1 = (dig['P3'] * var1 * var1 / 16384. + dig['P2'] * var1) / 524288.
var1 = (1. + var1 / 32768.) * dig['P1']
if var1 == 0:
return 0.
else:
pressure = 1048576.0 - raw_pressure
pressure = ((pressure - var2 / 4096.) * 6250.) / var1
var1 = dig['P9'] * pressure * pressure / 2147483648.
var2 = pressure * dig['P8'] / 32768.
var3 = (pressure / 256.) * (pressure / 256.) * (pressure / 256.) * (
dig['P10'] / 131072.)
return pressure + (var1 + var2 + var3 + (dig['P7'] * 128.)) / 16.
def _compensate_humidity_bme280(self, raw_humidity):
dig = self.dig
t_fine = self.t_fine
humidity = t_fine - 76800.
h1 = (
raw_humidity - (
dig['H4'] * 64. + dig['H5'] / 16384. * humidity))
h2 = (dig['H2'] / 65536. * (1. + dig['H6'] / 67108864. * humidity *
(1. + dig['H3'] / 67108864. * humidity)))
humidity = h1 * h2
humidity = humidity * (1. - dig['H1'] * humidity / 524288.)
return min(100., max(0., humidity))
def _compensate_humidity_bme680(self, raw_humidity):
dig = self.dig
temp_comp = self.temp
var1 = raw_humidity - (
(dig['H1'] * 16.) + ((dig['H3'] / 2.) * temp_comp))
var2 = var1 * ((dig['H2'] / 262144.) *
(1. + ((dig['H4'] / 16384.) * temp_comp) +
((dig['H5'] / 1048576.) * temp_comp * temp_comp)))
var3 = dig['H6'] / 16384.
var4 = dig['H7'] / 2097152.
humidity = var2 + ((var3 + (var4 * temp_comp)) * var2 * var2)
return min(100., max(0., humidity))
def _compensate_gas(self, gas_raw, gas_range):
gas_switching_error = self.read_register('RANGE_SWITCHING_ERROR', 1)[0]
var1 = (1340. + 5. * gas_switching_error) * \
BME680_GAS_CONSTANTS[gas_range][0]
gas = var1 * BME680_GAS_CONSTANTS[gas_range][1] / (
gas_raw - 512. + var1)
return gas
def _calculate_gas_heater_resistance(self, target_temp):
amb_temp = self.temp
heater_data = self.read_register('RES_HEAT_VAL', 3)
res_heat_val = get_signed_byte(heater_data[0])
res_heat_range = (heater_data[2] & 0x30) >> 4
dig = self.dig
var1 = (dig['G1'] / 16.) + 49.
var2 = ((dig['G2'] / 32768.) * 0.0005) + 0.00235
var3 = dig['G3'] / 1024.
var4 = var1 * (1. + (var2 * target_temp))
var5 = var4 + (var3 * amb_temp)
res_heat = (3.4 * ((var5 * (4. / (4. + res_heat_range))
* (1. / (1. + (res_heat_val * 0.002)))) - 25))
return int(res_heat)
def _calculate_gas_heater_duration(self, duration_ms):
if duration_ms >= 4032:
duration_reg = 0xff
else:
factor = 0
while duration_ms > 0x3F:
duration_ms //= 4
factor += 1
duration_reg = duration_ms + (factor * 64)
return duration_reg
def read_id(self):
# read chip id register
regs = [BME_CHIP_ID_REG]
params = self.i2c.i2c_read(regs, 1)
return bytearray(params['response'])[0]
def read_register(self, reg_name, read_len):
# read a single register
regs = [self.chip_registers[reg_name]]
params = self.i2c.i2c_read(regs, read_len)
return bytearray(params['response'])
def write_register(self, reg_name, data):
if type(data) is not list:
data = [data]
reg = self.chip_registers[reg_name]
data.insert(0, reg)
self.i2c.i2c_write(data)
def get_status(self, eventtime):
data = {
'temperature': round(self.temp, 2),
'pressure': self.pressure
}
if self.chip_type in ('BME280', 'BME680'):
data['humidity'] = self.humidity
if self.chip_type == 'BME680':
data['gas'] = self.gas
return data
def load_config(config):
# Register sensor
pheaters = config.get_printer().load_object(config, "heaters")
pheaters.add_sensor_factory("BME280", BME280)

BIN
klippy/extras/bme280.pyc Normal file

Binary file not shown.

View File

@@ -0,0 +1,27 @@
# Support for custom board pin aliases
#
# Copyright (C) 2019-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class PrinterBoardAliases:
def __init__(self, config):
ppins = config.get_printer().lookup_object('pins')
mcu_names = config.getlist('mcu', ('mcu',))
pin_resolvers = [ppins.get_pin_resolver(n) for n in mcu_names]
options = ["aliases"] + config.get_prefix_options("aliases_")
for opt in options:
aliases = config.getlists(opt, seps=('=', ','), count=2)
for name, value in aliases:
if value.startswith('<') and value.endswith('>'):
for pin_resolver in pin_resolvers:
pin_resolver.reserve_pin(name, value)
else:
for pin_resolver in pin_resolvers:
pin_resolver.alias_pin(name, value)
def load_config(config):
return PrinterBoardAliases(config)
def load_config_prefix(config):
return PrinterBoardAliases(config)

Binary file not shown.

252
klippy/extras/bus.py Normal file
View File

@@ -0,0 +1,252 @@
# Helper code for SPI and I2C bus communication
#
# Copyright (C) 2018,2019 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import mcu
def resolve_bus_name(mcu, param, bus):
# Find enumerations for the given bus
enumerations = mcu.get_enumerations()
enums = enumerations.get(param, enumerations.get('bus'))
if enums is None:
if bus is None:
return 0
return bus
# Verify bus is a valid enumeration
ppins = mcu.get_printer().lookup_object("pins")
mcu_name = mcu.get_name()
if bus is None:
rev_enums = {v: k for k, v in enums.items()}
if 0 not in rev_enums:
raise ppins.error("Must specify %s on mcu '%s'" % (param, mcu_name))
bus = rev_enums[0]
if bus not in enums:
raise ppins.error("Unknown %s '%s'" % (param, bus))
# Check for reserved bus pins
constants = mcu.get_constants()
reserve_pins = constants.get('BUS_PINS_%s' % (bus,), None)
pin_resolver = ppins.get_pin_resolver(mcu_name)
if reserve_pins is not None:
for pin in reserve_pins.split(','):
pin_resolver.reserve_pin(pin, bus)
return bus
######################################################################
# SPI
######################################################################
# Helper code for working with devices connected to an MCU via an SPI bus
class MCU_SPI:
def __init__(self, mcu, bus, pin, mode, speed, sw_pins=None,
cs_active_high=False):
self.mcu = mcu
self.bus = bus
# Config SPI object (set all CS pins high before spi_set_bus commands)
self.oid = mcu.create_oid()
if pin is None:
mcu.add_config_cmd("config_spi_without_cs oid=%d" % (self.oid,))
else:
mcu.add_config_cmd("config_spi oid=%d pin=%s cs_active_high=%d"
% (self.oid, pin, cs_active_high))
# Generate SPI bus config message
if sw_pins is not None:
self.config_fmt = (
"spi_set_software_bus oid=%d"
" miso_pin=%s mosi_pin=%s sclk_pin=%s mode=%d rate=%d"
% (self.oid, sw_pins[0], sw_pins[1], sw_pins[2], mode, speed))
else:
self.config_fmt = (
"spi_set_bus oid=%d spi_bus=%%s mode=%d rate=%d"
% (self.oid, mode, speed))
self.cmd_queue = mcu.alloc_command_queue()
mcu.register_config_callback(self.build_config)
self.spi_send_cmd = self.spi_transfer_cmd = None
def setup_shutdown_msg(self, shutdown_seq):
shutdown_msg = "".join(["%02x" % (x,) for x in shutdown_seq])
self.mcu.add_config_cmd(
"config_spi_shutdown oid=%d spi_oid=%d shutdown_msg=%s"
% (self.mcu.create_oid(), self.oid, shutdown_msg))
def get_oid(self):
return self.oid
def get_mcu(self):
return self.mcu
def get_command_queue(self):
return self.cmd_queue
def build_config(self):
if '%' in self.config_fmt:
bus = resolve_bus_name(self.mcu, "spi_bus", self.bus)
self.config_fmt = self.config_fmt % (bus,)
self.mcu.add_config_cmd(self.config_fmt)
self.spi_send_cmd = self.mcu.lookup_command(
"spi_send oid=%c data=%*s", cq=self.cmd_queue)
self.spi_transfer_cmd = self.mcu.lookup_query_command(
"spi_transfer oid=%c data=%*s",
"spi_transfer_response oid=%c response=%*s", oid=self.oid,
cq=self.cmd_queue)
def spi_send(self, data, minclock=0, reqclock=0):
if self.spi_send_cmd is None:
# Send setup message via mcu initialization
data_msg = "".join(["%02x" % (x,) for x in data])
self.mcu.add_config_cmd("spi_send oid=%d data=%s" % (
self.oid, data_msg), is_init=True)
return
self.spi_send_cmd.send([self.oid, data],
minclock=minclock, reqclock=reqclock)
def spi_transfer(self, data, minclock=0, reqclock=0):
return self.spi_transfer_cmd.send([self.oid, data],
minclock=minclock, reqclock=reqclock)
def spi_transfer_with_preface(self, preface_data, data,
minclock=0, reqclock=0):
return self.spi_transfer_cmd.send_with_preface(
self.spi_send_cmd, [self.oid, preface_data], [self.oid, data],
minclock=minclock, reqclock=reqclock)
# Helper to setup an spi bus from settings in a config section
def MCU_SPI_from_config(config, mode, pin_option="cs_pin",
default_speed=100000, share_type=None,
cs_active_high=False):
# Determine pin from config
ppins = config.get_printer().lookup_object("pins")
cs_pin = config.get(pin_option)
cs_pin_params = ppins.lookup_pin(cs_pin, share_type=share_type)
pin = cs_pin_params['pin']
if pin == 'None':
ppins.reset_pin_sharing(cs_pin_params)
pin = None
# Load bus parameters
mcu = cs_pin_params['chip']
speed = config.getint('spi_speed', default_speed, minval=100000)
if config.get('spi_software_sclk_pin', None) is not None:
sw_pin_names = ['spi_software_%s_pin' % (name,)
for name in ['miso', 'mosi', 'sclk']]
sw_pin_params = [ppins.lookup_pin(config.get(name), share_type=name)
for name in sw_pin_names]
for pin_params in sw_pin_params:
if pin_params['chip'] != mcu:
raise ppins.error("%s: spi pins must be on same mcu" % (
config.get_name(),))
sw_pins = tuple([pin_params['pin'] for pin_params in sw_pin_params])
bus = None
else:
bus = config.get('spi_bus', None)
sw_pins = None
# Create MCU_SPI object
return MCU_SPI(mcu, bus, pin, mode, speed, sw_pins, cs_active_high)
######################################################################
# I2C
######################################################################
# Helper code for working with devices connected to an MCU via an I2C bus
class MCU_I2C:
def __init__(self, mcu, bus, addr, speed):
self.mcu = mcu
self.bus = bus
self.i2c_address = addr
self.oid = self.mcu.create_oid()
self.config_fmt = "config_i2c oid=%d i2c_bus=%%s rate=%d address=%d" % (
self.oid, speed, addr)
self.cmd_queue = self.mcu.alloc_command_queue()
self.mcu.register_config_callback(self.build_config)
self.i2c_write_cmd = self.i2c_read_cmd = self.i2c_modify_bits_cmd = None
def get_oid(self):
return self.oid
def get_mcu(self):
return self.mcu
def get_i2c_address(self):
return self.i2c_address
def get_command_queue(self):
return self.cmd_queue
def build_config(self):
bus = resolve_bus_name(self.mcu, "i2c_bus", self.bus)
self.mcu.add_config_cmd(self.config_fmt % (bus,))
self.i2c_write_cmd = self.mcu.lookup_command(
"i2c_write oid=%c data=%*s", cq=self.cmd_queue)
self.i2c_read_cmd = self.mcu.lookup_query_command(
"i2c_read oid=%c reg=%*s read_len=%u",
"i2c_read_response oid=%c response=%*s", oid=self.oid,
cq=self.cmd_queue)
self.i2c_modify_bits_cmd = self.mcu.lookup_command(
"i2c_modify_bits oid=%c reg=%*s clear_set_bits=%*s",
cq=self.cmd_queue)
def i2c_write(self, data, minclock=0, reqclock=0):
if self.i2c_write_cmd is None:
# Send setup message via mcu initialization
data_msg = "".join(["%02x" % (x,) for x in data])
self.mcu.add_config_cmd("i2c_write oid=%d data=%s" % (
self.oid, data_msg), is_init=True)
return
self.i2c_write_cmd.send([self.oid, data],
minclock=minclock, reqclock=reqclock)
def i2c_read(self, write, read_len):
return self.i2c_read_cmd.send([self.oid, write, read_len])
def i2c_modify_bits(self, reg, clear_bits, set_bits,
minclock=0, reqclock=0):
clearset = clear_bits + set_bits
if self.i2c_modify_bits_cmd is None:
# Send setup message via mcu initialization
reg_msg = "".join(["%02x" % (x,) for x in reg])
clearset_msg = "".join(["%02x" % (x,) for x in clearset])
self.mcu.add_config_cmd(
"i2c_modify_bits oid=%d reg=%s clear_set_bits=%s" % (
self.oid, reg_msg, clearset_msg), is_init=True)
return
self.i2c_modify_bits_cmd.send([self.oid, reg, clearset],
minclock=minclock, reqclock=reqclock)
def MCU_I2C_from_config(config, default_addr=None, default_speed=100000):
# Load bus parameters
printer = config.get_printer()
i2c_mcu = mcu.get_printer_mcu(printer, config.get('i2c_mcu', 'mcu'))
speed = config.getint('i2c_speed', default_speed, minval=100000)
bus = config.get('i2c_bus', None)
if default_addr is None:
addr = config.getint('i2c_address', minval=0, maxval=127)
else:
addr = config.getint('i2c_address', default_addr, minval=0, maxval=127)
# Create MCU_I2C object
return MCU_I2C(i2c_mcu, bus, addr, speed)
######################################################################
# Bus synchronized digital outputs
######################################################################
# Helper code for a gpio that updates on a cmd_queue
class MCU_bus_digital_out:
def __init__(self, mcu, pin_desc, cmd_queue=None, value=0):
self.mcu = mcu
self.oid = mcu.create_oid()
ppins = mcu.get_printer().lookup_object('pins')
pin_params = ppins.lookup_pin(pin_desc)
if pin_params['chip'] is not mcu:
raise ppins.error("Pin %s must be on mcu %s" % (
pin_desc, mcu.get_name()))
mcu.add_config_cmd("config_digital_out oid=%d pin=%s value=%d"
" default_value=%d max_duration=%d"
% (self.oid, pin_params['pin'], value, value, 0))
mcu.register_config_callback(self.build_config)
if cmd_queue is None:
cmd_queue = mcu.alloc_command_queue()
self.cmd_queue = cmd_queue
self.update_pin_cmd = None
def get_oid(self):
return self.oid
def get_mcu(self):
return self.mcu
def get_command_queue(self):
return self.cmd_queue
def build_config(self):
self.update_pin_cmd = self.mcu.lookup_command(
"update_digital_out oid=%c value=%c", cq=self.cmd_queue)
def update_digital_out(self, value, minclock=0, reqclock=0):
if self.update_pin_cmd is None:
# Send setup message via mcu initialization
self.mcu.add_config_cmd("update_digital_out oid=%c value=%c"
% (self.oid, not not value))
return
self.update_pin_cmd.send([self.oid, not not value],
minclock=minclock, reqclock=reqclock)

BIN
klippy/extras/bus.pyc Normal file

Binary file not shown.

306
klippy/extras/buttons.py Normal file
View File

@@ -0,0 +1,306 @@
# Support for button detection and callbacks
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
######################################################################
# Button state tracking
######################################################################
QUERY_TIME = .002
RETRANSMIT_COUNT = 50
class MCU_buttons:
def __init__(self, printer, mcu):
self.reactor = printer.get_reactor()
self.mcu = mcu
self.mcu.register_config_callback(self.build_config)
self.pin_list = []
self.callbacks = []
self.invert = self.last_button = 0
self.ack_cmd = None
self.ack_count = 0
def setup_buttons(self, pins, callback):
mask = 0
shift = len(self.pin_list)
for pin_params in pins:
if pin_params['invert']:
self.invert |= 1 << len(self.pin_list)
mask |= 1 << len(self.pin_list)
self.pin_list.append((pin_params['pin'], pin_params['pullup']))
self.callbacks.append((mask, shift, callback))
def build_config(self):
if not self.pin_list:
return
self.oid = self.mcu.create_oid()
self.mcu.add_config_cmd("config_buttons oid=%d button_count=%d" % (
self.oid, len(self.pin_list)))
for i, (pin, pull_up) in enumerate(self.pin_list):
self.mcu.add_config_cmd(
"buttons_add oid=%d pos=%d pin=%s pull_up=%d" % (
self.oid, i, pin, pull_up), is_init=True)
cmd_queue = self.mcu.alloc_command_queue()
self.ack_cmd = self.mcu.lookup_command(
"buttons_ack oid=%c count=%c", cq=cmd_queue)
clock = self.mcu.get_query_slot(self.oid)
rest_ticks = self.mcu.seconds_to_clock(QUERY_TIME)
self.mcu.add_config_cmd(
"buttons_query oid=%d clock=%d"
" rest_ticks=%d retransmit_count=%d invert=%d" % (
self.oid, clock, rest_ticks, RETRANSMIT_COUNT,
self.invert), is_init=True)
self.mcu.register_response(self.handle_buttons_state,
"buttons_state", self.oid)
def handle_buttons_state(self, params):
# Expand the message ack_count from 8-bit
ack_count = self.ack_count
ack_diff = (ack_count - params['ack_count']) & 0xff
if ack_diff & 0x80:
ack_diff -= 0x100
msg_ack_count = ack_count - ack_diff
# Determine new buttons
buttons = bytearray(params['state'])
new_count = msg_ack_count + len(buttons) - self.ack_count
if new_count <= 0:
return
new_buttons = buttons[-new_count:]
# Send ack to MCU
self.ack_cmd.send([self.oid, new_count])
self.ack_count += new_count
# Call self.handle_button() with this event in main thread
for nb in new_buttons:
self.reactor.register_async_callback(
(lambda e, s=self, b=nb: s.handle_button(e, b)))
def handle_button(self, eventtime, button):
button ^= self.invert
changed = button ^ self.last_button
for mask, shift, callback in self.callbacks:
if changed & mask:
callback(eventtime, (button & mask) >> shift)
self.last_button = button
######################################################################
# ADC button tracking
######################################################################
ADC_REPORT_TIME = 0.015
ADC_DEBOUNCE_TIME = 0.025
ADC_SAMPLE_TIME = 0.001
ADC_SAMPLE_COUNT = 6
class MCU_ADC_buttons:
def __init__(self, printer, pin, pullup):
self.reactor = printer.get_reactor()
self.buttons = []
self.last_button = None
self.last_pressed = None
self.last_debouncetime = 0
self.pullup = pullup
self.pin = pin
self.min_value = 999999999999.9
self.max_value = 0.
ppins = printer.lookup_object('pins')
self.mcu_adc = ppins.setup_pin('adc', self.pin)
self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback)
query_adc = printer.lookup_object('query_adc')
query_adc.register_adc('adc_button:' + pin.strip(), self.mcu_adc)
def setup_button(self, min_value, max_value, callback):
self.min_value = min(self.min_value, min_value)
self.max_value = max(self.max_value, max_value)
self.buttons.append((min_value, max_value, callback))
def adc_callback(self, read_time, read_value):
adc = max(.00001, min(.99999, read_value))
value = self.pullup * adc / (1.0 - adc)
# Determine button pressed
btn = None
if self.min_value <= value <= self.max_value:
for i, (min_value, max_value, cb) in enumerate(self.buttons):
if min_value < value < max_value:
btn = i
break
# If the button changed, due to noise or pressing:
if btn != self.last_button:
# reset the debouncing timer
self.last_debouncetime = read_time
# button debounce check & new button pressed
if ((read_time - self.last_debouncetime) >= ADC_DEBOUNCE_TIME
and self.last_button == btn and self.last_pressed != btn):
# release last_pressed
if self.last_pressed is not None:
self.call_button(self.last_pressed, False)
self.last_pressed = None
if btn is not None:
self.call_button(btn, True)
self.last_pressed = btn
self.last_button = btn
def call_button(self, button, state):
minval, maxval, callback = self.buttons[button]
self.reactor.register_async_callback(
(lambda e, cb=callback, s=state: cb(e, s)))
######################################################################
# Rotary Encoders
######################################################################
# Rotary encoder handler https://github.com/brianlow/Rotary
# Copyright 2011 Ben Buxton (bb@cactii.net).
# Licenced under the GNU GPL Version 3.
class BaseRotaryEncoder:
R_START = 0x0
R_DIR_CW = 0x10
R_DIR_CCW = 0x20
R_DIR_MSK = 0x30
def __init__(self, cw_callback, ccw_callback):
self.cw_callback = cw_callback
self.ccw_callback = ccw_callback
self.encoder_state = self.R_START
def encoder_callback(self, eventtime, state):
es = self.ENCODER_STATES[self.encoder_state & 0xf][state & 0x3]
self.encoder_state = es
if es & self.R_DIR_MSK == self.R_DIR_CW:
self.cw_callback(eventtime)
elif es & self.R_DIR_MSK == self.R_DIR_CCW:
self.ccw_callback(eventtime)
class FullStepRotaryEncoder(BaseRotaryEncoder):
R_CW_FINAL = 0x1
R_CW_BEGIN = 0x2
R_CW_NEXT = 0x3
R_CCW_BEGIN = 0x4
R_CCW_FINAL = 0x5
R_CCW_NEXT = 0x6
# Use the full-step state table (emits a code at 00 only)
ENCODER_STATES = (
# R_START
(BaseRotaryEncoder.R_START, R_CW_BEGIN, R_CCW_BEGIN,
BaseRotaryEncoder.R_START),
# R_CW_FINAL
(R_CW_NEXT, BaseRotaryEncoder.R_START, R_CW_FINAL,
BaseRotaryEncoder.R_START | BaseRotaryEncoder.R_DIR_CW),
# R_CW_BEGIN
(R_CW_NEXT, R_CW_BEGIN, BaseRotaryEncoder.R_START,
BaseRotaryEncoder.R_START),
# R_CW_NEXT
(R_CW_NEXT, R_CW_BEGIN, R_CW_FINAL, BaseRotaryEncoder.R_START),
# R_CCW_BEGIN
(R_CCW_NEXT, BaseRotaryEncoder.R_START, R_CCW_BEGIN,
BaseRotaryEncoder.R_START),
# R_CCW_FINAL
(R_CCW_NEXT, R_CCW_FINAL, BaseRotaryEncoder.R_START,
BaseRotaryEncoder.R_START | BaseRotaryEncoder.R_DIR_CCW),
# R_CCW_NEXT
(R_CCW_NEXT, R_CCW_FINAL, R_CCW_BEGIN, BaseRotaryEncoder.R_START)
)
class HalfStepRotaryEncoder(BaseRotaryEncoder):
# Use the half-step state table (emits a code at 00 and 11)
R_CCW_BEGIN = 0x1
R_CW_BEGIN = 0x2
R_START_M = 0x3
R_CW_BEGIN_M = 0x4
R_CCW_BEGIN_M = 0x5
ENCODER_STATES = (
# R_START (00)
(R_START_M, R_CW_BEGIN, R_CCW_BEGIN, BaseRotaryEncoder.R_START),
# R_CCW_BEGIN
(R_START_M | BaseRotaryEncoder.R_DIR_CCW, BaseRotaryEncoder.R_START,
R_CCW_BEGIN, BaseRotaryEncoder.R_START),
# R_CW_BEGIN
(R_START_M | BaseRotaryEncoder.R_DIR_CW, R_CW_BEGIN,
BaseRotaryEncoder.R_START, BaseRotaryEncoder.R_START),
# R_START_M (11)
(R_START_M, R_CCW_BEGIN_M, R_CW_BEGIN_M, BaseRotaryEncoder.R_START),
# R_CW_BEGIN_M
(R_START_M, R_START_M, R_CW_BEGIN_M,
BaseRotaryEncoder.R_START | BaseRotaryEncoder.R_DIR_CW),
# R_CCW_BEGIN_M
(R_START_M, R_CCW_BEGIN_M, R_START_M,
BaseRotaryEncoder.R_START | BaseRotaryEncoder.R_DIR_CCW),
)
######################################################################
# Button registration code
######################################################################
class PrinterButtons:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.load_object(config, 'query_adc')
self.mcu_buttons = {}
self.adc_buttons = {}
def register_adc_button(self, pin, min_val, max_val, pullup, callback):
adc_buttons = self.adc_buttons.get(pin)
if adc_buttons is None:
self.adc_buttons[pin] = adc_buttons = MCU_ADC_buttons(
self.printer, pin, pullup)
adc_buttons.setup_button(min_val, max_val, callback)
def register_adc_button_push(self, pin, min_val, max_val, pullup, callback):
def helper(eventtime, state, callback=callback):
if state:
callback(eventtime)
self.register_adc_button(pin, min_val, max_val, pullup, helper)
def register_buttons(self, pins, callback):
# Parse pins
ppins = self.printer.lookup_object('pins')
mcu = mcu_name = None
pin_params_list = []
for pin in pins:
pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True)
if mcu is not None and pin_params['chip'] != mcu:
raise ppins.error("button pins must be on same mcu")
mcu = pin_params['chip']
mcu_name = pin_params['chip_name']
pin_params_list.append(pin_params)
# Register pins and callback with the appropriate MCU
mcu_buttons = self.mcu_buttons.get(mcu_name)
if (mcu_buttons is None
or len(mcu_buttons.pin_list) + len(pin_params_list) > 8):
self.mcu_buttons[mcu_name] = mcu_buttons = MCU_buttons(
self.printer, mcu)
mcu_buttons.setup_buttons(pin_params_list, callback)
def register_rotary_encoder(self, pin1, pin2, cw_callback, ccw_callback,
steps_per_detent):
if steps_per_detent == 2:
re = HalfStepRotaryEncoder(cw_callback, ccw_callback)
elif steps_per_detent == 4:
re = FullStepRotaryEncoder(cw_callback, ccw_callback)
else:
raise self.printer.config_error(
"%d steps per detent not supported" % steps_per_detent)
self.register_buttons([pin1, pin2], re.encoder_callback)
def register_button_push(self, pin, callback):
def helper(eventtime, state, callback=callback):
if state:
callback(eventtime)
self.register_buttons([pin], helper)
def load_config(config):
return PrinterButtons(config)

BIN
klippy/extras/buttons.pyc Normal file

Binary file not shown.

View File

@@ -0,0 +1,24 @@
# Support for tracking canbus node ids
#
# Copyright (C) 2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class PrinterCANBus:
def __init__(self, config):
self.printer = config.get_printer()
self.ids = {}
def add_uuid(self, config, canbus_uuid, canbus_iface):
if canbus_uuid in self.ids:
raise config.error("Duplicate canbus_uuid")
new_id = len(self.ids)
self.ids[canbus_uuid] = new_id
return new_id
def get_nodeid(self, canbus_uuid):
if canbus_uuid not in self.ids:
raise self.printer.config_error("Unknown canbus_uuid %s"
% (canbus_uuid,))
return self.ids[canbus_uuid]
def load_config(config):
return PrinterCANBus(config)

View File

@@ -0,0 +1,71 @@
# Support a fan for cooling the MCU whenever a stepper or heater is on
#
# Copyright (C) 2019 Nils Friedchen <nils.friedchen@googlemail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import fan
PIN_MIN_TIME = 0.100
class ControllerFan:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:ready", self.handle_ready)
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.stepper_names = config.getlist("stepper", None)
self.stepper_enable = self.printer.load_object(config, 'stepper_enable')
self.printer.load_object(config, 'heaters')
self.heaters = []
self.fan = fan.Fan(config)
self.fan_speed = config.getfloat('fan_speed', default=1.,
minval=0., maxval=1.)
self.idle_speed = config.getfloat(
'idle_speed', default=self.fan_speed, minval=0., maxval=1.)
self.idle_timeout = config.getint("idle_timeout", default=30, minval=0)
self.heater_names = config.getlist("heater", ("extruder",))
self.last_on = self.idle_timeout
self.last_speed = 0.
def handle_connect(self):
# Heater lookup
pheaters = self.printer.lookup_object('heaters')
self.heaters = [pheaters.lookup_heater(n) for n in self.heater_names]
# Stepper lookup
all_steppers = self.stepper_enable.get_steppers()
if self.stepper_names is None:
self.stepper_names = all_steppers
return
if not all(x in all_steppers for x in self.stepper_names):
raise self.printer.config_error(
"One or more of these steppers are unknown: "
"%s (valid steppers are: %s)"
% (self.stepper_names, ", ".join(all_steppers)))
def handle_ready(self):
reactor = self.printer.get_reactor()
reactor.register_timer(self.callback, reactor.monotonic()+PIN_MIN_TIME)
def get_status(self, eventtime):
return self.fan.get_status(eventtime)
def callback(self, eventtime):
speed = 0.
active = False
for name in self.stepper_names:
active |= self.stepper_enable.lookup_enable(name).is_motor_enabled()
for heater in self.heaters:
_, target_temp = heater.get_temp(eventtime)
if target_temp:
active = True
if active:
self.last_on = 0
speed = self.fan_speed
elif self.last_on < self.idle_timeout:
speed = self.idle_speed
self.last_on += 1
if speed != self.last_speed:
self.last_speed = speed
curtime = self.printer.get_reactor().monotonic()
print_time = self.fan.get_mcu().estimated_print_time(curtime)
self.fan.set_speed(print_time + PIN_MIN_TIME, speed)
return eventtime + 1.
def load_config_prefix(config):
return ControllerFan(config)

View File

@@ -0,0 +1,25 @@
# SPI DAC DAC084S085 implementation
#
# Copyright (C) 2021 Lorenzo Franco <lorenzo.franco@lorenzing.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import bus
class dac084S085:
def __init__(self, config):
self.spi = bus.MCU_SPI_from_config(
config, 1, pin_option="enable_pin", default_speed=10000000)
scale = config.getfloat('scale', 1., above=0.)
for chan, name in enumerate("ABCD"):
val = config.getfloat('channel_%s' % (name,), None,
minval=0., maxval=scale)
if val is not None:
self.set_register(chan, int(val * 255. / scale))
def set_register(self, chan, value):
b1 = (chan << 6) | (1 << 4) | ((value >> 4) & 0x0f)
b2 = (value << 4) & 0xf0
self.spi.spi_send([b1, b2])
def load_config_prefix(config):
return dac084S085(config)

View File

@@ -0,0 +1,54 @@
# A simple timer for executing gcode templates
#
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
class DelayedGcode:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.name = config.get_name().split()[1]
self.gcode = self.printer.lookup_object('gcode')
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.timer_gcode = gcode_macro.load_template(config, 'gcode')
self.duration = config.getfloat('initial_duration', 0., minval=0.)
self.timer_handler = None
self.inside_timer = self.repeat = False
self.printer.register_event_handler("klippy:ready", self._handle_ready)
self.gcode.register_mux_command(
"UPDATE_DELAYED_GCODE", "ID", self.name,
self.cmd_UPDATE_DELAYED_GCODE,
desc=self.cmd_UPDATE_DELAYED_GCODE_help)
def _handle_ready(self):
waketime = self.reactor.NEVER
if self.duration:
waketime = self.reactor.monotonic() + self.duration
self.timer_handler = self.reactor.register_timer(
self._gcode_timer_event, waketime)
def _gcode_timer_event(self, eventtime):
self.inside_timer = True
try:
self.gcode.run_script(self.timer_gcode.render())
except Exception:
logging.exception("Script running error")
nextwake = self.reactor.NEVER
if self.repeat:
nextwake = eventtime + self.duration
self.inside_timer = self.repeat = False
return nextwake
cmd_UPDATE_DELAYED_GCODE_help = "Update the duration of a delayed_gcode"
def cmd_UPDATE_DELAYED_GCODE(self, gcmd):
self.duration = gcmd.get_float('DURATION', minval=0.)
if self.inside_timer:
self.repeat = (self.duration != 0.)
else:
waketime = self.reactor.NEVER
if self.duration:
waketime = self.reactor.monotonic() + self.duration
self.reactor.update_timer(self.timer_handler, waketime)
def load_config_prefix(config):
return DelayedGcode(config)

Binary file not shown.

View File

@@ -0,0 +1,287 @@
# Delta calibration support
#
# Copyright (C) 2017-2019 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging, collections
import mathutil
from . import probe
# A "stable position" is a 3-tuple containing the number of steps
# taken since hitting the endstop on each delta tower. Delta
# calibration uses this coordinate system because it allows a position
# to be described independent of the software parameters.
# Load a stable position from a config entry
def load_config_stable(config, option):
return config.getfloatlist(option, count=3)
######################################################################
# Delta calibration object
######################################################################
# The angles and distances of the calibration object found in
# docs/prints/calibrate_size.stl
MeasureAngles = [210., 270., 330., 30., 90., 150.]
MeasureOuterRadius = 65
MeasureRidgeRadius = 5. - .5
# How much to prefer a distance measurement over a height measurement
MEASURE_WEIGHT = 0.5
# Convert distance measurements made on the calibration object to
# 3-tuples of (actual_distance, stable_position1, stable_position2)
def measurements_to_distances(measured_params, delta_params):
# Extract params
mp = measured_params
dp = delta_params
scale = mp['SCALE'][0]
cpw = mp['CENTER_PILLAR_WIDTHS']
center_widths = [cpw[0], cpw[2], cpw[1], cpw[0], cpw[2], cpw[1]]
center_dists = [od - cw
for od, cw in zip(mp['CENTER_DISTS'], center_widths)]
outer_dists = [
od - opw
for od, opw in zip(mp['OUTER_DISTS'], mp['OUTER_PILLAR_WIDTHS']) ]
# Convert angles in degrees to an XY multiplier
obj_angles = list(map(math.radians, MeasureAngles))
xy_angles = list(zip(map(math.cos, obj_angles), map(math.sin, obj_angles)))
# Calculate stable positions for center measurements
inner_ridge = MeasureRidgeRadius * scale
inner_pos = [(ax * inner_ridge, ay * inner_ridge, 0.)
for ax, ay in xy_angles]
outer_ridge = (MeasureOuterRadius + MeasureRidgeRadius) * scale
outer_pos = [(ax * outer_ridge, ay * outer_ridge, 0.)
for ax, ay in xy_angles]
center_positions = [
(cd, dp.calc_stable_position(ip), dp.calc_stable_position(op))
for cd, ip, op in zip(center_dists, inner_pos, outer_pos)]
# Calculate positions of outer measurements
outer_center = MeasureOuterRadius * scale
start_pos = [(ax * outer_center, ay * outer_center) for ax, ay in xy_angles]
shifted_angles = xy_angles[2:] + xy_angles[:2]
first_pos = [(ax * inner_ridge + spx, ay * inner_ridge + spy, 0.)
for (ax, ay), (spx, spy) in zip(shifted_angles, start_pos)]
second_pos = [(ax * outer_ridge + spx, ay * outer_ridge + spy, 0.)
for (ax, ay), (spx, spy) in zip(shifted_angles, start_pos)]
outer_positions = [
(od, dp.calc_stable_position(fp), dp.calc_stable_position(sp))
for od, fp, sp in zip(outer_dists, first_pos, second_pos)]
return center_positions + outer_positions
######################################################################
# Delta Calibrate class
######################################################################
class DeltaCalibrate:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
# Calculate default probing points
radius = config.getfloat('radius', above=0.)
points = [(0., 0.)]
scatter = [.95, .90, .85, .70, .75, .80]
for i in range(6):
r = math.radians(90. + 60. * i)
dist = radius * scatter[i]
points.append((math.cos(r) * dist, math.sin(r) * dist))
self.probe_helper = probe.ProbePointsHelper(
config, self.probe_finalize, default_points=points)
self.probe_helper.minimum_points(3)
# Restore probe stable positions
self.last_probe_positions = []
for i in range(999):
height = config.getfloat("height%d" % (i,), None)
if height is None:
break
height_pos = load_config_stable(config, "height%d_pos" % (i,))
self.last_probe_positions.append((height, height_pos))
# Restore manually entered heights
self.manual_heights = []
for i in range(999):
height = config.getfloat("manual_height%d" % (i,), None)
if height is None:
break
height_pos = load_config_stable(config, "manual_height%d_pos"
% (i,))
self.manual_heights.append((height, height_pos))
# Restore distance measurements
self.delta_analyze_entry = {'SCALE': (1.,)}
self.last_distances = []
for i in range(999):
dist = config.getfloat("distance%d" % (i,), None)
if dist is None:
break
distance_pos1 = load_config_stable(config, "distance%d_pos1" % (i,))
distance_pos2 = load_config_stable(config, "distance%d_pos2" % (i,))
self.last_distances.append((dist, distance_pos1, distance_pos2))
# Register gcode commands
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command('DELTA_CALIBRATE', self.cmd_DELTA_CALIBRATE,
desc=self.cmd_DELTA_CALIBRATE_help)
self.gcode.register_command('DELTA_ANALYZE', self.cmd_DELTA_ANALYZE,
desc=self.cmd_DELTA_ANALYZE_help)
def handle_connect(self):
kin = self.printer.lookup_object('toolhead').get_kinematics()
if not hasattr(kin, "get_calibration"):
raise self.printer.config_error(
"Delta calibrate is only for delta printers")
def save_state(self, probe_positions, distances, delta_params):
# Save main delta parameters
configfile = self.printer.lookup_object('configfile')
delta_params.save_state(configfile)
# Save probe stable positions
section = 'delta_calibrate'
configfile.remove_section(section)
for i, (z_offset, spos) in enumerate(probe_positions):
configfile.set(section, "height%d" % (i,), z_offset)
configfile.set(section, "height%d_pos" % (i,),
"%.3f,%.3f,%.3f" % tuple(spos))
# Save manually entered heights
for i, (z_offset, spos) in enumerate(self.manual_heights):
configfile.set(section, "manual_height%d" % (i,), z_offset)
configfile.set(section, "manual_height%d_pos" % (i,),
"%.3f,%.3f,%.3f" % tuple(spos))
# Save distance measurements
for i, (dist, spos1, spos2) in enumerate(distances):
configfile.set(section, "distance%d" % (i,), dist)
configfile.set(section, "distance%d_pos1" % (i,),
"%.3f,%.3f,%.3f" % tuple(spos1))
configfile.set(section, "distance%d_pos2" % (i,),
"%.3f,%.3f,%.3f" % tuple(spos2))
def probe_finalize(self, offsets, positions):
# Convert positions into (z_offset, stable_position) pairs
z_offset = offsets[2]
kin = self.printer.lookup_object('toolhead').get_kinematics()
delta_params = kin.get_calibration()
probe_positions = [(z_offset, delta_params.calc_stable_position(p))
for p in positions]
# Perform analysis
self.calculate_params(probe_positions, self.last_distances)
def calculate_params(self, probe_positions, distances):
height_positions = self.manual_heights + probe_positions
# Setup for coordinate descent analysis
kin = self.printer.lookup_object('toolhead').get_kinematics()
orig_delta_params = odp = kin.get_calibration()
adj_params, params = odp.coordinate_descent_params(distances)
logging.info("Calculating delta_calibrate with:\n%s\n%s\n"
"Initial delta_calibrate parameters: %s",
height_positions, distances, params)
z_weight = 1.
if distances:
z_weight = len(distances) / (MEASURE_WEIGHT * len(probe_positions))
# Perform coordinate descent
def delta_errorfunc(params):
try:
# Build new delta_params for params under test
delta_params = orig_delta_params.new_calibration(params)
getpos = delta_params.get_position_from_stable
# Calculate z height errors
total_error = 0.
for z_offset, stable_pos in height_positions:
x, y, z = getpos(stable_pos)
total_error += (z - z_offset)**2
total_error *= z_weight
# Calculate distance errors
for dist, stable_pos1, stable_pos2 in distances:
x1, y1, z1 = getpos(stable_pos1)
x2, y2, z2 = getpos(stable_pos2)
d = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
total_error += (d - dist)**2
return total_error
except ValueError:
return 9999999999999.9
new_params = mathutil.background_coordinate_descent(
self.printer, adj_params, params, delta_errorfunc)
# Log and report results
logging.info("Calculated delta_calibrate parameters: %s", new_params)
new_delta_params = orig_delta_params.new_calibration(new_params)
for z_offset, spos in height_positions:
logging.info("height orig: %.6f new: %.6f goal: %.6f",
orig_delta_params.get_position_from_stable(spos)[2],
new_delta_params.get_position_from_stable(spos)[2],
z_offset)
for dist, spos1, spos2 in distances:
x1, y1, z1 = orig_delta_params.get_position_from_stable(spos1)
x2, y2, z2 = orig_delta_params.get_position_from_stable(spos2)
orig_dist = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
x1, y1, z1 = new_delta_params.get_position_from_stable(spos1)
x2, y2, z2 = new_delta_params.get_position_from_stable(spos2)
new_dist = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
logging.info("distance orig: %.6f new: %.6f goal: %.6f",
orig_dist, new_dist, dist)
# Store results for SAVE_CONFIG
self.save_state(probe_positions, distances, new_delta_params)
self.gcode.respond_info(
"The SAVE_CONFIG command will update the printer config file\n"
"with these parameters and restart the printer.")
cmd_DELTA_CALIBRATE_help = "Delta calibration script"
def cmd_DELTA_CALIBRATE(self, gcmd):
self.probe_helper.start_probe(gcmd)
def add_manual_height(self, height):
# Determine current location of toolhead
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
kin = toolhead.get_kinematics()
kin_spos = {s.get_name(): s.get_commanded_position()
for s in kin.get_steppers()}
kin_pos = kin.calc_position(kin_spos)
# Convert location to a stable position
delta_params = kin.get_calibration()
stable_pos = tuple(delta_params.calc_stable_position(kin_pos))
# Add to list of manual heights
self.manual_heights.append((height, stable_pos))
self.gcode.respond_info(
"Adding manual height: %.3f,%.3f,%.3f is actually z=%.3f"
% (kin_pos[0], kin_pos[1], kin_pos[2], height))
def do_extended_calibration(self):
# Extract distance positions
if len(self.delta_analyze_entry) <= 1:
distances = self.last_distances
elif len(self.delta_analyze_entry) < 5:
raise self.gcode.error("Not all measurements provided")
else:
kin = self.printer.lookup_object('toolhead').get_kinematics()
delta_params = kin.get_calibration()
distances = measurements_to_distances(
self.delta_analyze_entry, delta_params)
if not self.last_probe_positions:
raise self.gcode.error(
"Must run basic calibration with DELTA_CALIBRATE first")
# Perform analysis
self.calculate_params(self.last_probe_positions, distances)
cmd_DELTA_ANALYZE_help = "Extended delta calibration tool"
def cmd_DELTA_ANALYZE(self, gcmd):
# Check for manual height entry
mheight = gcmd.get_float('MANUAL_HEIGHT', None)
if mheight is not None:
self.add_manual_height(mheight)
return
# Parse distance measurements
args = {'CENTER_DISTS': 6, 'CENTER_PILLAR_WIDTHS': 3,
'OUTER_DISTS': 6, 'OUTER_PILLAR_WIDTHS': 6, 'SCALE': 1}
for name, count in args.items():
data = gcmd.get(name, None)
if data is None:
continue
try:
parts = list(map(float, data.split(',')))
except:
raise gcmd.error("Unable to parse parameter '%s'" % (name,))
if len(parts) != count:
raise gcmd.error("Parameter '%s' must have %d values"
% (name, count))
self.delta_analyze_entry[name] = parts
logging.info("DELTA_ANALYZE %s = %s", name, parts)
# Perform analysis if requested
action = gcmd.get('CALIBRATE', None)
if action is not None:
if action != 'extended':
raise gcmd.error("Unknown calibrate action")
self.do_extended_calibration()
def load_config(config):
return DeltaCalibrate(config)

View File

@@ -0,0 +1,21 @@
# Package definition for the extras/display directory
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import display
def load_config(config):
return display.load_config(config)
def load_config_prefix(config):
if not config.has_section('display'):
raise config.error(
"A primary [display] section must be defined in printer.cfg "
"to use auxilary displays")
name = config.get_name().split()[-1]
if name == "display":
raise config.error(
"Section name [display display] is not valid. "
"Please choose a different postfix.")
return display.load_config(config)

Binary file not shown.

View File

@@ -0,0 +1,461 @@
# This file defines the default layout of the printer's lcd display.
# It is not necessary to edit this file to change the display.
# Instead, one may override any of the sections defined here by
# defining a section with the same name in the main printer.cfg config
# file.
######################################################################
# Helper macros for showing common screen values
######################################################################
[display_template _heater_temperature]
param_heater_name: "extruder"
text:
{% if param_heater_name in printer %}
{% set heater = printer[param_heater_name] %}
# Show glyph
{% if param_heater_name == "heater_bed" %}
{% if heater.target %}
{% set frame = (printer.toolhead.estimated_print_time|int % 2) + 1 %}
~bed_heat{frame}~
{% else %}
~bed~
{% endif %}
{% else %}
~extruder~
{% endif %}
# Show temperature
{ "%3.0f" % (heater.temperature,) }
# Optionally show target
{% if heater.target and (heater.temperature - heater.target)|abs > 2 %}
~right_arrow~
{ "%0.0f" % (heater.target,) }
{% endif %}
~degrees~
{% endif %}
[display_template _fan_speed]
text:
{% if 'fan' in printer %}
{% set speed = printer.fan.speed %}
{% if speed %}
{% set frame = (printer.toolhead.estimated_print_time|int % 2) + 1 %}
~fan{frame}~
{% else %}
~fan1~
{% endif %}
{ "{:>4.0%}".format(speed) }
{% endif %}
[display_template _printing_time]
text:
{% set ptime = printer.idle_timeout.printing_time %}
{ "%02d:%02d" % (ptime // (60 * 60), (ptime // 60) % 60) }
[display_template _print_status]
text:
{% if printer.display_status.message %}
{ printer.display_status.message }
{% elif printer.idle_timeout.printing_time %}
{% set pos = printer.toolhead.position %}
{ "X%-4.0fY%-4.0fZ%-5.2f" % (pos.x, pos.y, pos.z) }
{% else %}
Ready
{% endif %}
######################################################################
# Default 16x4 display
######################################################################
[display_data _default_16x4 extruder]
position: 0, 0
text:
{% set active_extruder = printer.toolhead.extruder %}
{ render("_heater_temperature", param_heater_name=active_extruder) }
[display_data _default_16x4 fan]
position: 0, 10
text: { render("_fan_speed") }
[display_data _default_16x4 heater_bed]
position: 1, 0
text: { render("_heater_temperature", param_heater_name="heater_bed") }
[display_data _default_16x4 speed_factor]
position: 1, 10
text:
~feedrate~
{ "{:>4.0%}".format(printer.gcode_move.speed_factor) }
[display_data _default_16x4 print_progress]
position: 2, 0
text: { "{:^10.0%}".format(printer.display_status.progress) }
[display_data _default_16x4 progress_bar]
position: 2, 1 # Draw graphical progress bar after text is written
text: { draw_progress_bar(2, 0, 10, printer.display_status.progress) }
[display_data _default_16x4 printing_time]
position: 2, 10
text: { "%6s" % (render("_printing_time").strip(),) }
[display_data _default_16x4 print_status]
position: 3, 0
text: { render("_print_status") }
######################################################################
# Alternative 16x4 layout for multi-extruders
######################################################################
[display_data _multiextruder_16x4 extruder]
position: 0, 0
text: { render("_heater_temperature", param_heater_name="extruder") }
[display_data _multiextruder_16x4 fan]
position: 0, 10
text: { render("_fan_speed") }
[display_data _multiextruder_16x4 extruder1]
position: 1, 0
text: { render("_heater_temperature", param_heater_name="extruder1") }
[display_data _multiextruder_16x4 print_progress]
position: 1, 10
text: { "{:^6.0%}".format(printer.display_status.progress) }
[display_data _multiextruder_16x4 progress_bar]
position: 1, 11 # Draw graphical progress bar after text is written
text: { draw_progress_bar(1, 10, 6, printer.display_status.progress) }
[display_data _multiextruder_16x4 heater_bed]
position: 2, 0
text: { render("_heater_temperature", param_heater_name="heater_bed") }
[display_data _multiextruder_16x4 printing_time]
position: 2, 10
text: { "%6s" % (render("_printing_time").strip(),) }
[display_data _multiextruder_16x4 print_status]
position: 3, 0
text: { render("_print_status") }
######################################################################
# Default 20x4 display
######################################################################
[display_data _default_20x4 extruder]
position: 0, 0
text: { render("_heater_temperature", param_heater_name="extruder") }
[display_data _default_20x4 heater_bed]
position: 0, 10
text: { render("_heater_temperature", param_heater_name="heater_bed") }
[display_data _default_20x4 extruder1]
position: 1, 0
text: { render("_heater_temperature", param_heater_name="extruder1") }
[display_data _default_20x4 fan]
position: 1, 10
text:
{% if 'fan' in printer %}
{ "Fan {:^4.0%}".format(printer.fan.speed) }
{% endif %}
[display_data _default_20x4 speed_factor]
position: 2, 0
text:
~feedrate~
{ "{:^4.0%}".format(printer.gcode_move.speed_factor) }
[display_data _default_20x4 print_progress]
position: 2, 8
text:
{% if 'virtual_sdcard' in printer and printer.virtual_sdcard.progress %}
~sd~
{% else %}
~usb~
{% endif %}
{ "{:^4.0%}".format(printer.display_status.progress) }
[display_data _default_20x4 printing_time]
position: 2, 14
text:
~clock~
{ render("_printing_time") }
[display_data _default_20x4 print_status]
position: 3, 0
text: { render("_print_status") }
######################################################################
# Default 16x4 glyphs
######################################################################
[display_glyph extruder]
data:
................
................
..************..
.....******.....
..************..
.....******.....
..************..
................
....********....
....******.*....
....********....
................
......****......
.......**.......
................
................
[display_glyph bed]
data:
................
................
................
................
................
................
................
................
................
................
................
...*********....
..*.........*...
.*************..
................
................
[display_glyph bed_heat1]
data:
................
................
..*....*....*...
.*....*....*....
..*....*....*...
...*....*....*..
..*....*....*...
.*....*....*....
..*....*....*...
................
................
...*********....
..*.........*...
.*************..
................
................
[display_glyph bed_heat2]
data:
................
................
..*....*....*...
...*....*....*..
..*....*....*...
.*....*....*....
..*....*....*...
...*....*....*..
..*....*....*...
................
................
...*********....
..*.........*...
.*************..
................
................
[display_glyph fan1]
data:
................
................
....***.........
...****....**...
...****...****..
....***..*****..
.....*....****..
.......**.......
.......**.......
..****....*.....
..*****..***....
..****...****...
...**....****...
.........***....
................
................
[display_glyph fan2]
data:
................
................
.......****.....
.......****.....
.......***......
..**...**.......
..***...........
..****.**.****..
..****.**.****..
...........***..
.......**...**..
......***.......
.....****.......
.....****.......
................
................
[display_glyph feedrate]
data:
................
................
***.***.***.**..
*...*...*...*.*.
**..**..**..*.*.
*...*...*...*.*.
*...***.***.**..
................
**...*..***.***.
*.*.*.*..*..*...
**..***..*..**..
*.*.*.*..*..*...
*.*.*.*..*..***.
................
................
................
# In addition to the above glyphs, 16x4 displays also have the
# following hard-coded single character glyphs: right_arrow, degrees.
######################################################################
# Default 20x4 glyphs
######################################################################
[display_glyph extruder]
hd44780_slot: 0
hd44780_data:
..*..
.*.*.
.*.*.
.*.*.
.*.*.
*...*
*...*
.***.
[display_glyph bed]
hd44780_slot: 1
hd44780_data:
.....
*****
*.*.*
*...*
*.*.*
*****
.....
.....
[display_glyph bed_heat1]
hd44780_slot: 1
hd44780_data:
.*..*
*..*.
.*..*
*..*.
.....
*****
.....
.....
[display_glyph bed_heat2]
hd44780_slot: 1
hd44780_data:
*..*.
.*..*
*..*.
.*..*
.....
*****
.....
.....
[display_glyph fan]
hd44780_slot: 2
hd44780_data:
.....
*..**
**.*.
..*..
.*.**
**..*
.....
.....
[display_glyph feedrate]
hd44780_slot: 3
hd44780_data:
***..
*....
**...
*.***
..*.*
..**.
..*.*
.....
[display_glyph clock]
hd44780_slot: 4
hd44780_data:
.....
.***.
*..**
*.*.*
*...*
.***.
.....
.....
[display_glyph degrees]
hd44780_slot: 5
hd44780_data:
.**..
*..*.
*..*.
.**..
.....
.....
.....
.....
[display_glyph usb]
hd44780_slot: 6
hd44780_data:
.***.
.***.
.***.
*****
*****
*****
..*..
..*..
[display_glyph sd]
hd44780_slot: 6
hd44780_data:
.....
..***
.****
*****
*****
*****
*****
.....
# In addition to the above glyphs, 20x4 displays also have the
# following hard-coded glyphs: right_arrow.

View File

@@ -0,0 +1,271 @@
# Basic LCD display support
#
# Copyright (C) 2018-2022 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Aleph Objects, Inc <marcio@alephobjects.com>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, os, ast
from . import hd44780, hd44780_spi, st7920, uc1701, menu
# Normal time between each screen redraw
REDRAW_TIME = 0.500
# Minimum time between screen redraws
REDRAW_MIN_TIME = 0.100
LCD_chips = {
'st7920': st7920.ST7920, 'emulated_st7920': st7920.EmulatedST7920,
'hd44780': hd44780.HD44780, 'uc1701': uc1701.UC1701,
'ssd1306': uc1701.SSD1306, 'sh1106': uc1701.SH1106,
'hd44780_spi': hd44780_spi.hd44780_spi
}
# Storage of [display_template my_template] config sections
class DisplayTemplate:
def __init__(self, config):
self.printer = config.get_printer()
name_parts = config.get_name().split()
if len(name_parts) != 2:
raise config.error("Section name '%s' is not valid"
% (config.get_name(),))
self.name = name_parts[1]
self.params = {}
for option in config.get_prefix_options('param_'):
try:
self.params[option] = ast.literal_eval(config.get(option))
except ValueError as e:
raise config.error(
"Option '%s' in section '%s' is not a valid literal" % (
option, config.get_name()))
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.template = gcode_macro.load_template(config, 'text')
def get_params(self):
return self.params
def render(self, context, **kwargs):
params = dict(self.params)
params.update(**kwargs)
if len(params) != len(self.params):
raise self.printer.command_error(
"Invalid parameter to display_template %s" % (self.name,))
context = dict(context)
context.update(params)
return self.template.render(context)
# Store [display_data my_group my_item] sections (one instance per group name)
class DisplayGroup:
def __init__(self, config, name, data_configs):
# Load and parse the position of display_data items
items = []
for c in data_configs:
pos = c.get('position')
try:
row, col = [int(v.strip()) for v in pos.split(',')]
except:
raise config.error("Unable to parse 'position' in section '%s'"
% (c.get_name(),))
items.append((row, col, c.get_name()))
# Load all templates and store sorted by display position
configs_by_name = {c.get_name(): c for c in data_configs}
printer = config.get_printer()
gcode_macro = printer.load_object(config, 'gcode_macro')
self.data_items = []
for row, col, name in sorted(items):
c = configs_by_name[name]
if c.get('text'):
template = gcode_macro.load_template(c, 'text')
self.data_items.append((row, col, template))
def show(self, display, templates, eventtime):
context = self.data_items[0][2].create_template_context(eventtime)
context['draw_progress_bar'] = display.draw_progress_bar
def render(name, **kwargs):
return templates[name].render(context, **kwargs)
context['render'] = render
for row, col, template in self.data_items:
text = template.render(context)
display.draw_text(row, col, text.replace('\n', ''), eventtime)
context.clear() # Remove circular references for better gc
# Global cache of DisplayTemplate, DisplayGroup, and glyphs
class PrinterDisplayTemplate:
def __init__(self, config):
self.printer = config.get_printer()
self.display_templates = {}
self.display_data_groups = {}
self.display_glyphs = {}
self.load_config(config)
def get_display_templates(self):
return self.display_templates
def get_display_data_groups(self):
return self.display_data_groups
def get_display_glyphs(self):
return self.display_glyphs
def _parse_glyph(self, config, glyph_name, data, width, height):
glyph_data = []
for line in data.split('\n'):
line = line.strip().replace('.', '0').replace('*', '1')
if not line:
continue
if len(line) != width or line.replace('0', '').replace('1', ''):
raise config.error("Invalid glyph line in %s" % (glyph_name,))
glyph_data.append(int(line, 2))
if len(glyph_data) != height:
raise config.error("Glyph %s incorrect lines" % (glyph_name,))
return glyph_data
def load_config(self, config):
# Load default display config file
pconfig = self.printer.lookup_object('configfile')
filename = os.path.join(os.path.dirname(__file__), 'display.cfg')
try:
dconfig = pconfig.read_config(filename)
except Exception:
raise self.printer.config_error("Cannot load config '%s'"
% (filename,))
# Load display_template sections
dt_main = config.get_prefix_sections('display_template ')
dt_main_names = { c.get_name(): 1 for c in dt_main }
dt_def = [c for c in dconfig.get_prefix_sections('display_template ')
if c.get_name() not in dt_main_names]
for c in dt_main + dt_def:
dt = DisplayTemplate(c)
self.display_templates[dt.name] = dt
# Load display_data sections
dd_main = config.get_prefix_sections('display_data ')
dd_main_names = { c.get_name(): 1 for c in dd_main }
dd_def = [c for c in dconfig.get_prefix_sections('display_data ')
if c.get_name() not in dd_main_names]
groups = {}
for c in dd_main + dd_def:
name_parts = c.get_name().split()
if len(name_parts) != 3:
raise config.error("Section name '%s' is not valid"
% (c.get_name(),))
groups.setdefault(name_parts[1], []).append(c)
for group_name, data_configs in groups.items():
dg = DisplayGroup(config, group_name, data_configs)
self.display_data_groups[group_name] = dg
# Load display glyphs
dg_prefix = 'display_glyph '
self.display_glyphs = icons = {}
dg_main = config.get_prefix_sections(dg_prefix)
dg_main_names = {c.get_name(): 1 for c in dg_main}
dg_def = [c for c in dconfig.get_prefix_sections(dg_prefix)
if c.get_name() not in dg_main_names]
for dg in dg_main + dg_def:
glyph_name = dg.get_name()[len(dg_prefix):]
data = dg.get('data', None)
if data is not None:
idata = self._parse_glyph(config, glyph_name, data, 16, 16)
icon1 = [(bits >> 8) & 0xff for bits in idata]
icon2 = [bits & 0xff for bits in idata]
icons.setdefault(glyph_name, {})['icon16x16'] = (icon1, icon2)
data = dg.get('hd44780_data', None)
if data is not None:
slot = dg.getint('hd44780_slot', minval=0, maxval=7)
idata = self._parse_glyph(config, glyph_name, data, 5, 8)
icons.setdefault(glyph_name, {})['icon5x8'] = (slot, idata)
def lookup_display_templates(config):
printer = config.get_printer()
dt = printer.lookup_object("display_template", None)
if dt is None:
dt = PrinterDisplayTemplate(config)
printer.add_object("display_template", dt)
return dt
class PrinterLCD:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
# Load low-level lcd handler
self.lcd_chip = config.getchoice('lcd_type', LCD_chips)(config)
# Load menu and display_status
self.menu = None
name = config.get_name()
if name == 'display':
# only load menu for primary display
self.menu = menu.MenuManager(config, self)
self.printer.load_object(config, "display_status")
# Configurable display
templates = lookup_display_templates(config)
self.display_templates = templates.get_display_templates()
self.display_data_groups = templates.get_display_data_groups()
self.lcd_chip.set_glyphs(templates.get_display_glyphs())
dgroup = "_default_16x4"
if self.lcd_chip.get_dimensions()[0] == 20:
dgroup = "_default_20x4"
dgroup = config.get('display_group', dgroup)
self.show_data_group = self.display_data_groups.get(dgroup)
if self.show_data_group is None:
raise config.error("Unknown display_data group '%s'" % (dgroup,))
# Screen updating
self.printer.register_event_handler("klippy:ready", self.handle_ready)
self.screen_update_timer = self.reactor.register_timer(
self.screen_update_event)
self.redraw_request_pending = False
self.redraw_time = 0.
# Register g-code commands
gcode = self.printer.lookup_object("gcode")
gcode.register_mux_command('SET_DISPLAY_GROUP', 'DISPLAY', name,
self.cmd_SET_DISPLAY_GROUP,
desc=self.cmd_SET_DISPLAY_GROUP_help)
if name == 'display':
gcode.register_mux_command('SET_DISPLAY_GROUP', 'DISPLAY', None,
self.cmd_SET_DISPLAY_GROUP)
def get_dimensions(self):
return self.lcd_chip.get_dimensions()
def handle_ready(self):
self.lcd_chip.init()
# Start screen update timer
self.reactor.update_timer(self.screen_update_timer, self.reactor.NOW)
# Screen updating
def screen_update_event(self, eventtime):
if self.redraw_request_pending:
self.redraw_request_pending = False
self.redraw_time = eventtime + REDRAW_MIN_TIME
self.lcd_chip.clear()
# update menu component
if self.menu is not None:
ret = self.menu.screen_update_event(eventtime)
if ret:
self.lcd_chip.flush()
return eventtime + REDRAW_TIME
# Update normal display
try:
self.show_data_group.show(self, self.display_templates, eventtime)
except:
logging.exception("Error during display screen update")
self.lcd_chip.flush()
return eventtime + REDRAW_TIME
def request_redraw(self):
if self.redraw_request_pending:
return
self.redraw_request_pending = True
self.reactor.update_timer(self.screen_update_timer, self.redraw_time)
def draw_text(self, row, col, mixed_text, eventtime):
pos = col
for i, text in enumerate(mixed_text.split('~')):
if i & 1 == 0:
# write text
self.lcd_chip.write_text(pos, row, text.encode())
pos += len(text)
else:
# write glyph
pos += self.lcd_chip.write_glyph(pos, row, text)
return pos
def draw_progress_bar(self, row, col, width, value):
pixels = -1 << int(width * 8 * (1. - value) + .5)
pixels |= (1 << (width * 8 - 1)) | 1
for i in range(width):
data = [0xff] + [(pixels >> (i * 8)) & 0xff] * 14 + [0xff]
self.lcd_chip.write_graphics(col + width - 1 - i, row, data)
return ""
cmd_SET_DISPLAY_GROUP_help = "Set the active display group"
def cmd_SET_DISPLAY_GROUP(self, gcmd):
group = gcmd.get('GROUP')
new_dg = self.display_data_groups.get(group)
if new_dg is None:
raise gcmd.error("Unknown display_data group '%s'" % (group,))
self.show_data_group = new_dg
def load_config(config):
return PrinterLCD(config)

Binary file not shown.

View File

@@ -0,0 +1,276 @@
# Fonts for connected displays
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
######################################################################
# Font - VGA 8x14, Row Major, MSB, 2 bytes padding
#
# Font comes from fntcol16.zip package found at:
# ftp://ftp.simtel.net/pub/simtelnet/msdos/screen/fntcol16.zip
# (c) Joseph Gil
#
# Indivdual fonts are public domain
######################################################################
VGA_FONT = [
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x7e\x81\xa5\x81\x81\xbd\x99\x81\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x7e\xff\xdb\xff\xff\xc3\xe7\xff\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x6c\xfe\xfe\xfe\xfe\x7c\x38\x10\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x10\x38\x7c\xfe\x7c\x38\x10\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x3c\xe7\xe7\xe7\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x7e\xff\xff\x7e\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x18\x3c\x3c\x18\x00\x00\x00\x00\x00\x00',
b'\x00\xff\xff\xff\xff\xff\xe7\xc3\xc3\xe7\xff\xff\xff\xff\xff\x00',
b'\x00\x00\x00\x00\x00\x3c\x66\x42\x42\x66\x3c\x00\x00\x00\x00\x00',
b'\x00\xff\xff\xff\xff\xc3\x99\xbd\xbd\x99\xc3\xff\xff\xff\xff\x00',
b'\x00\x00\x00\x1e\x0e\x1a\x32\x78\xcc\xcc\xcc\x78\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x66\x66\x66\x3c\x18\x7e\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x3f\x33\x3f\x30\x30\x30\x70\xf0\xe0\x00\x00\x00\x00',
b'\x00\x00\x00\x7f\x63\x7f\x63\x63\x63\x67\xe7\xe6\xc0\x00\x00\x00',
b'\x00\x00\x00\x18\x18\xdb\x3c\xe7\x3c\xdb\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x80\xc0\xe0\xf8\xfe\xf8\xe0\xc0\x80\x00\x00\x00\x00',
b'\x00\x00\x00\x02\x06\x0e\x3e\xfe\x3e\x0e\x06\x02\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x66\x66\x66\x66\x66\x66\x00\x66\x66\x00\x00\x00\x00',
b'\x00\x00\x00\x7f\xdb\xdb\xdb\x7b\x1b\x1b\x1b\x1b\x00\x00\x00\x00',
b'\x00\x00\x7c\xc6\x60\x38\x6c\xc6\xc6\x6c\x38\x0c\xc6\x7c\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x7e\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x7e\x18\x18\x18\x18\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x18\x18\x18\x18\x18\x7e\x3c\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x18\x0c\xfe\x0c\x18\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x30\x60\xfe\x60\x30\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xc0\xc0\xc0\xfe\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x28\x6c\xfe\x6c\x28\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x10\x38\x38\x7c\x7c\xfe\xfe\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\xfe\xfe\x7c\x7c\x38\x38\x10\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x3c\x3c\x18\x18\x00\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x66\x66\x66\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x6c\x6c\xfe\x6c\x6c\x6c\xfe\x6c\x6c\x00\x00\x00\x00',
b'\x00\x18\x18\x7c\xc6\xc2\xc0\x7c\x06\x86\xc6\x7c\x18\x18\x00\x00',
b'\x00\x00\x00\x00\x00\xc2\xc6\x0c\x18\x30\x66\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\x6c\x38\x76\xdc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x30\x30\x30\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x0c\x18\x30\x30\x30\x30\x30\x18\x0c\x00\x00\x00\x00',
b'\x00\x00\x00\x30\x18\x0c\x0c\x0c\x0c\x0c\x18\x30\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x66\x3c\xff\x3c\x66\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x18\x30\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x02\x06\x0c\x18\x30\x60\xc0\x80\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xce\xde\xf6\xe6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x38\x78\x18\x18\x18\x18\x18\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\x06\x0c\x18\x30\x60\xc6\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\x06\x06\x3c\x06\x06\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x0c\x1c\x3c\x6c\xcc\xfe\x0c\x0c\x1e\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\xc0\xc0\xc0\xfc\x06\x06\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x60\xc0\xc0\xfc\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\xc6\x06\x0c\x18\x30\x30\x30\x30\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\xc6\x7c\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\xc6\x7e\x06\x06\x0c\x78\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x30\x00\x00\x00\x00',
b'\x00\x00\x00\x06\x0c\x18\x30\x60\x30\x18\x0c\x06\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7e\x00\x00\x7e\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x60\x30\x18\x0c\x06\x0c\x18\x30\x60\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\x0c\x18\x18\x00\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\xde\xde\xde\xdc\xc0\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\xfc\x66\x66\x66\x7c\x66\x66\x66\xfc\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x66\xc2\xc0\xc0\xc0\xc2\x66\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\xf8\x6c\x66\x66\x66\x66\x66\x6c\xf8\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\x66\x62\x68\x78\x68\x62\x66\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\x66\x62\x68\x78\x68\x60\x60\xf0\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x66\xc2\xc0\xc0\xde\xc6\x66\x3a\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\xc6\xc6\xfe\xc6\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x1e\x0c\x0c\x0c\x0c\x0c\xcc\xcc\x78\x00\x00\x00\x00',
b'\x00\x00\x00\xe6\x66\x6c\x6c\x78\x6c\x6c\x66\xe6\x00\x00\x00\x00',
b'\x00\x00\x00\xf0\x60\x60\x60\x60\x60\x62\x66\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xee\xfe\xfe\xd6\xc6\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\xc6\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00\x00',
b'\x00\x00\x00\xfc\x66\x66\x66\x7c\x60\x60\x60\xf0\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xd6\xde\x7c\x0c\x0e\x00\x00\x00',
b'\x00\x00\x00\xfc\x66\x66\x66\x7c\x6c\x66\x66\xe6\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\x60\x38\x0c\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x7e\x7e\x5a\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\x6c\x38\x10\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\xc6\xc6\xd6\xd6\xfe\x7c\x6c\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\x6c\x38\x38\x38\x6c\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x66\x66\x66\x66\x3c\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\xc6\x8c\x18\x30\x60\xc2\xc6\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x30\x30\x30\x30\x30\x30\x30\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x80\xc0\xe0\x70\x38\x1c\x0e\x06\x02\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x3c\x00\x00\x00\x00',
b'\x00\x10\x38\x6c\xc6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00',
b'\x00\x30\x30\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\xe0\x60\x60\x78\x6c\x66\x66\x66\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7c\xc6\xc0\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x1c\x0c\x0c\x3c\x6c\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\xf0\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\x7c\x0c\xcc\x78\x00\x00',
b'\x00\x00\x00\xe0\x60\x60\x6c\x76\x66\x66\x66\xe6\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x18\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x06\x06\x00\x0e\x06\x06\x06\x06\x66\x66\x3c\x00\x00',
b'\x00\x00\x00\xe0\x60\x60\x66\x6c\x78\x6c\x66\xe6\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xec\xfe\xd6\xd6\xd6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x66\x66\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x7c\x60\x60\xf0\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\x7c\x0c\x0c\x1e\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xdc\x76\x66\x60\x60\xf0\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7c\xc6\x70\x1c\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x10\x30\x30\xfc\x30\x30\x30\x36\x1c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x66\x66\x66\x66\x3c\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xc6\xc6\xd6\xd6\xfe\x6c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xc6\x6c\x38\x38\x6c\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xc6\xc6\xc6\xc6\x7e\x06\x0c\xf8\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xfe\xcc\x18\x30\x66\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x0e\x18\x18\x18\x70\x18\x18\x18\x0e\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x18\x18\x18\x00\x18\x18\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x70\x18\x18\x18\x0e\x18\x18\x18\x70\x00\x00\x00\x00',
b'\x00\x00\x00\x76\xdc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x10\x38\x6c\xc6\xc6\xfe\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x66\xc2\xc0\xc0\xc2\x66\x3c\x0c\x06\x7c\x00\x00',
b'\x00\x00\x00\xcc\xcc\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x0c\x18\x30\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x10\x38\x6c\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\xcc\xcc\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x38\x6c\x38\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x3c\x66\x60\x66\x3c\x0c\x06\x3c\x00\x00\x00',
b'\x00\x00\x10\x38\x6c\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\xcc\xcc\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x66\x66\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x18\x3c\x66\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\xc6\xc6\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\x00\x00\x00\x00',
b'\x00\x38\x6c\x38\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\x00\x00\x00\x00',
b'\x00\x18\x30\x60\x00\xfe\x66\x60\x7c\x60\x66\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\xcc\x76\x36\x7e\xd8\xd8\x6e\x00\x00\x00\x00',
b'\x00\x00\x00\x3e\x6c\xcc\xcc\xfe\xcc\xcc\xcc\xce\x00\x00\x00\x00',
b'\x00\x00\x10\x38\x6c\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x30\x78\xcc\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\x00\xc6\xc6\xc6\xc6\x7e\x06\x0c\x78\x00\x00',
b'\x00\x00\xc6\xc6\x38\x6c\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00\x00',
b'\x00\x00\xc6\xc6\x00\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x18\x18\x3c\x66\x60\x60\x66\x3c\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\xe6\xfc\x00\x00\x00\x00',
b'\x00\x00\x00\x66\x66\x3c\x18\x7e\x18\x7e\x18\x18\x00\x00\x00\x00',
b'\x00\x00\xf8\xcc\xcc\xf8\xc4\xcc\xde\xcc\xcc\xc6\x00\x00\x00\x00',
b'\x00\x00\x0e\x1b\x18\x18\x18\x7e\x18\x18\x18\x18\xd8\x70\x00\x00',
b'\x00\x00\x18\x30\x60\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x0c\x18\x30\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x18\x30\x60\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x18\x30\x60\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x76\xdc\x00\xdc\x66\x66\x66\x66\x66\x00\x00\x00\x00',
b'\x00\x76\xdc\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x3c\x6c\x6c\x3e\x00\x7e\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x38\x6c\x6c\x38\x00\x7c\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x30\x30\x00\x30\x30\x60\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\xfe\xc0\xc0\xc0\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\xfe\x06\x06\x06\x00\x00\x00\x00\x00',
b'\x00\x00\xc0\xc0\xc6\xcc\xd8\x30\x60\xdc\x86\x0c\x18\x3e\x00\x00',
b'\x00\x00\xc0\xc0\xc6\xcc\xd8\x30\x66\xce\x9e\x3e\x06\x06\x00\x00',
b'\x00\x00\x00\x18\x18\x00\x18\x18\x3c\x3c\x3c\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x36\x6c\xd8\x6c\x36\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\xd8\x6c\x36\x6c\xd8\x00\x00\x00\x00\x00\x00',
b'\x00\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x00',
b'\x00\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x00',
b'\x00\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\xf8\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\xf8\x18\xf8\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\xf6\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x00\x00\x00\x00\x00\xf8\x18\xf8\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x36\x36\x36\x36\x36\xf6\x06\xf6\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x00\x00\x00\x00\x00\xfe\x06\xf6\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\xf6\x06\xfe\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\xfe\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x18\x18\x18\x18\x18\xf8\x18\xf8\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\x1f\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xff\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\x1f\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\xff\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x1f\x18\x1f\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\x37\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x37\x30\x3f\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x3f\x30\x37\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\xf7\x00\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xff\x00\xf7\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x37\x30\x37\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x00\x00\x00\x00\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x36\x36\x36\x36\x36\xf7\x00\xf7\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x18\x18\x18\x18\x18\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xff\x00\xff\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xff\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\x3f\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x18\x18\x18\x18\x18\x1f\x18\x1f\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x1f\x18\x1f\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x3f\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\xff\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x18\x18\x18\x18\x18\xff\x18\xff\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\xf8\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x18\x18\x18\x18\x18\x18\x00',
b'\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00',
b'\x00\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\x00',
b'\x00\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x00',
b'\x00\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x76\xdc\xd8\xd8\xdc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x7c\xc6\xfc\xc6\xc6\xfc\xc0\xc0\x40\x00\x00',
b'\x00\x00\x00\xfe\xc6\xc6\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\xfe\x6c\x6c\x6c\x6c\x6c\x6c\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\xc6\x60\x30\x18\x30\x60\xc6\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7e\xd8\xd8\xd8\xd8\x70\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x66\x66\x66\x66\x7c\x60\x60\xc0\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x76\xdc\x18\x18\x18\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x7e\x18\x3c\x66\x66\x66\x3c\x18\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\x6c\x38\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\xc6\xc6\xc6\x6c\x6c\x6c\xee\x00\x00\x00\x00',
b'\x00\x00\x00\x1e\x30\x18\x0c\x3e\x66\x66\x66\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7e\xdb\xdb\x7e\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x03\x06\x7e\xdb\xdb\xf3\x7e\x60\xc0\x00\x00\x00\x00',
b'\x00\x00\x00\x1c\x30\x60\x60\x7c\x60\x60\x30\x1c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x00\xfe\x00\x00\xfe\x00\x00\xfe\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\xff\x00\x00\x00\x00',
b'\x00\x00\x00\x30\x18\x0c\x06\x0c\x18\x30\x00\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x0c\x18\x30\x60\x30\x18\x0c\x00\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x0e\x1b\x1b\x18\x18\x18\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\x18\xd8\xd8\x70\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x18\x18\x00\x7e\x00\x18\x18\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x76\xdc\x00\x76\xdc\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x38\x6c\x6c\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x0f\x0c\x0c\x0c\x0c\x0c\xec\x6c\x3c\x1c\x00\x00\x00\x00',
b'\x00\x00\xd8\x6c\x6c\x6c\x6c\x6c\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x70\xd8\x30\x60\xc8\xf8\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x7c\x7c\x7c\x7c\x7c\x7c\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
]

Binary file not shown.

View File

@@ -0,0 +1,134 @@
# Support for HD44780 (20x4 text) LCD displays
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
LINE_LENGTH_DEFAULT=20
LINE_LENGTH_OPTIONS={16:16, 20:20}
TextGlyphs = { 'right_arrow': b'\x7e' }
HD44780_DELAY = .000040
class HD44780:
def __init__(self, config):
self.printer = config.get_printer()
# pin config
ppins = self.printer.lookup_object('pins')
pins = [ppins.lookup_pin(config.get(name + '_pin'))
for name in ['rs', 'e', 'd4', 'd5', 'd6', 'd7']]
self.hd44780_protocol_init = config.getboolean('hd44780_protocol_init',
True)
self.line_length = config.getchoice('line_length', LINE_LENGTH_OPTIONS,
LINE_LENGTH_DEFAULT)
mcu = None
for pin_params in pins:
if mcu is not None and pin_params['chip'] != mcu:
raise ppins.error("hd44780 all pins must be on same mcu")
mcu = pin_params['chip']
self.pins = [pin_params['pin'] for pin_params in pins]
self.mcu = mcu
self.oid = self.mcu.create_oid()
self.mcu.register_config_callback(self.build_config)
self.send_data_cmd = self.send_cmds_cmd = None
self.icons = {}
# framebuffers
self.text_framebuffers = [bytearray(b' '*2*self.line_length),
bytearray(b' '*2*self.line_length)]
self.glyph_framebuffer = bytearray(64)
self.all_framebuffers = [
# Text framebuffers
(self.text_framebuffers[0], bytearray(b'~'*2*self.line_length),
0x80),
(self.text_framebuffers[1], bytearray(b'~'*2*self.line_length),
0xc0),
# Glyph framebuffer
(self.glyph_framebuffer, bytearray(b'~'*64), 0x40) ]
def build_config(self):
self.mcu.add_config_cmd(
"config_hd44780 oid=%d rs_pin=%s e_pin=%s"
" d4_pin=%s d5_pin=%s d6_pin=%s d7_pin=%s delay_ticks=%d" % (
self.oid, self.pins[0], self.pins[1],
self.pins[2], self.pins[3], self.pins[4], self.pins[5],
self.mcu.seconds_to_clock(HD44780_DELAY)))
cmd_queue = self.mcu.alloc_command_queue()
self.send_cmds_cmd = self.mcu.lookup_command(
"hd44780_send_cmds oid=%c cmds=%*s", cq=cmd_queue)
self.send_data_cmd = self.mcu.lookup_command(
"hd44780_send_data oid=%c data=%*s", cq=cmd_queue)
def send(self, cmds, is_data=False):
cmd_type = self.send_cmds_cmd
if is_data:
cmd_type = self.send_data_cmd
cmd_type.send([self.oid, cmds], reqclock=BACKGROUND_PRIORITY_CLOCK)
#logging.debug("hd44780 %d %s", is_data, repr(cmds))
def flush(self):
# Find all differences in the framebuffers and send them to the chip
for new_data, old_data, fb_id in self.all_framebuffers:
if new_data == old_data:
continue
# Find the position of all changed bytes in this framebuffer
diffs = [[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data))
if n != o]
# Batch together changes that are close to each other
for i in range(len(diffs)-2, -1, -1):
pos, count = diffs[i]
nextpos, nextcount = diffs[i+1]
if pos + 4 >= nextpos and nextcount < 16:
diffs[i][1] = nextcount + (nextpos - pos)
del diffs[i+1]
# Transmit changes
for pos, count in diffs:
chip_pos = pos
self.send([fb_id + chip_pos])
self.send(new_data[pos:pos+count], is_data=True)
old_data[:] = new_data
def init(self):
curtime = self.printer.get_reactor().monotonic()
print_time = self.mcu.estimated_print_time(curtime)
# Program 4bit / 2-line mode and then issue 0x02 "Home" command
if self.hd44780_protocol_init:
init = [[0x33], [0x33], [0x32], [0x28, 0x28, 0x02]]
else:
init = [[0x02]]
# Reset (set positive direction ; enable display and hide cursor)
init.append([0x06, 0x0c])
for i, cmds in enumerate(init):
minclock = self.mcu.print_time_to_clock(print_time + i * .100)
self.send_cmds_cmd.send([self.oid, cmds], minclock=minclock)
self.flush()
def write_text(self, x, y, data):
if x + len(data) > self.line_length:
data = data[:self.line_length - min(x, self.line_length)]
pos = x + ((y & 0x02) >> 1) * self.line_length
self.text_framebuffers[y & 1][pos:pos+len(data)] = data
def set_glyphs(self, glyphs):
for glyph_name, glyph_data in glyphs.items():
data = glyph_data.get('icon5x8')
if data is not None:
self.icons[glyph_name] = data
def write_glyph(self, x, y, glyph_name):
data = self.icons.get(glyph_name)
if data is not None:
slot, bits = data
self.write_text(x, y, [slot])
self.glyph_framebuffer[slot * 8:(slot + 1) * 8] = bits
return 1
char = TextGlyphs.get(glyph_name)
if char is not None:
# Draw character
self.write_text(x, y, char)
return 1
return 0
def write_graphics(self, x, y, data):
pass
def clear(self):
spaces = b' ' * 2*self.line_length
self.text_framebuffers[0][:] = spaces
self.text_framebuffers[1][:] = spaces
def get_dimensions(self):
return (self.line_length, 4)

Binary file not shown.

View File

@@ -0,0 +1,125 @@
# Support for HD44780 (20x4 text) LCD displays
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
# Copyright (C) 2021 Marc-Andre Denis <marcadenis@msn.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from .. import bus
LINE_LENGTH_DEFAULT=20
LINE_LENGTH_OPTIONS={16:16, 20:20}
TextGlyphs = { 'right_arrow': b'\x7e' }
class hd44780_spi:
def __init__(self, config):
self.printer = config.get_printer()
self.hd44780_protocol_init = config.getboolean('hd44780_protocol_init',
True)
# spi config
self.spi = bus.MCU_SPI_from_config(
config, 0x00, pin_option="latch_pin")
self.mcu = self.spi.get_mcu()
#self.spi.spi_send([0x01,0xa0])
self.data_mask = (1<<1)
self.command_mask = 0
self.enable_mask = (1<<3)
self.icons = {}
self.line_length = config.getchoice('line_length', LINE_LENGTH_OPTIONS,
LINE_LENGTH_DEFAULT)
# framebuffers
self.text_framebuffers = [bytearray(b' '*2*self.line_length),
bytearray(b' '*2*self.line_length)]
self.glyph_framebuffer = bytearray(64)
self.all_framebuffers = [
# Text framebuffers
(self.text_framebuffers[0], bytearray(b'~'*2*self.line_length),
0x80),
(self.text_framebuffers[1], bytearray(b'~'*2*self.line_length),
0xc0),
# Glyph framebuffer
(self.glyph_framebuffer, bytearray(b'~'*64), 0x40) ]
def send_4_bits(self, cmd, is_data, minclock):
if is_data:
mask = self.data_mask
else:
mask = self.command_mask
self.spi.spi_send([(cmd & 0xF0) | mask], minclock)
self.spi.spi_send([(cmd & 0xF0) | mask | self.enable_mask], minclock)
self.spi.spi_send([(cmd & 0xF0) | mask], minclock)
def send(self, cmds, is_data=False, minclock=0):
for data in cmds:
self.send_4_bits(data,is_data,minclock)
self.send_4_bits(data<<4,is_data,minclock)
def flush(self):
# Find all differences in the framebuffers and send them to the chip
for new_data, old_data, fb_id in self.all_framebuffers:
if new_data == old_data:
continue
# Find the position of all changed bytes in this framebuffer
diffs = [[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data))
if n != o]
# Batch together changes that are close to each other
for i in range(len(diffs)-2, -1, -1):
pos, count = diffs[i]
nextpos, nextcount = diffs[i+1]
if pos + 4 >= nextpos and nextcount < 16:
diffs[i][1] = nextcount + (nextpos - pos)
del diffs[i+1]
# Transmit changes
for pos, count in diffs:
chip_pos = pos
self.send([fb_id + chip_pos])
self.send(new_data[pos:pos+count], is_data=True)
old_data[:] = new_data
def init(self):
curtime = self.printer.get_reactor().monotonic()
print_time = self.mcu.estimated_print_time(curtime)
# Program 4bit / 2-line mode and then issue 0x02 "Home" command
if self.hd44780_protocol_init:
init = [[0x33], [0x33], [0x32], [0x28, 0x28, 0x02]]
else:
init = [[0x02]]
# Reset (set positive direction ; enable display and hide cursor)
init.append([0x06, 0x0c])
for i, cmds in enumerate(init):
minclock = self.mcu.print_time_to_clock(print_time + i * .100)
self.send(cmds, minclock=minclock)
self.flush()
def write_text(self, x, y, data):
if x + len(data) > self.line_length:
data = data[:self.line_length - min(x, self.line_length)]
pos = x + ((y & 0x02) >> 1) * self.line_length
self.text_framebuffers[y & 1][pos:pos+len(data)] = data
def set_glyphs(self, glyphs):
for glyph_name, glyph_data in glyphs.items():
data = glyph_data.get('icon5x8')
if data is not None:
self.icons[glyph_name] = data
def write_glyph(self, x, y, glyph_name):
data = self.icons.get(glyph_name)
if data is not None:
slot, bits = data
self.write_text(x, y, [slot])
self.glyph_framebuffer[slot * 8:(slot + 1) * 8] = bits
return 1
char = TextGlyphs.get(glyph_name)
if char is not None:
# Draw character
self.write_text(x, y, char)
return 1
return 0
def write_graphics(self, x, y, data):
pass
def clear(self):
spaces = b' ' * 2*self.line_length
self.text_framebuffers[0][:] = spaces
self.text_framebuffers[1][:] = spaces
def get_dimensions(self):
return (self.line_length, 4)

Binary file not shown.

View File

@@ -0,0 +1,761 @@
# This file defines the default layout of the printer's menu.
# It is not necessary to edit this file to change the menu. Instead,
# one may override any of the sections defined here by defining a
# section with the same name in the main printer.cfg config file.
### DEFAULT MENU ###
# Main
# + Tune
# + Speed: 000%
# + Flow: 000%
# + Offset Z:00.00
# + OctoPrint
# + Pause printing
# + Resume printing
# + Abort printing
# + SD Card
# + Start printing
# + Resume printing
# + Pause printing
# + Cancel printing
# + ... (files)
# + Control
# + Home All
# + Home Z
# + Home X/Y
# + Steppers off
# + Fan: OFF
# + Fan speed: 000%
# + Lights: OFF
# + Lights: 000%
# + Move 10mm
# + Move X:000.0
# + Move Y:000.0
# + Move Z:000.0
# + Move E:+000.0
# + Move 1mm
# + Move X:000.0
# + Move Y:000.0
# + Move Z:000.0
# + Move E:+000.0
# + Move 0.1mm
# + Move X:000.0
# + Move Y:000.0
# + Move Z:000.0
# + Move E:+000.0
# + Temperature
# + Ex0:000 (0000)
# + Ex1:000 (0000)
# + Bed:000 (0000)
# + Preheat PLA
# + Preheat all
# + Preheat hotend
# + Preheat hotbed
# + Preheat ABS
# + Preheat all
# + Preheat hotend
# + Preheat hotbed
# + Cooldown
# + Cooldown all
# + Cooldown hotend
# + Cooldown hotbed
# + Filament
# + Ex0:000 (0000)
# + Load Fil. fast
# + Load Fil. slow
# + Unload Fil.fast
# + Unload Fil.slow
# + Feed: 000.0
# + Setup
# + Save config
# + Restart
# + Restart host
# + Restart FW
# + PID tuning
# + Tune Hotend PID
# + Tune Hotbed PID
# + Calibration
# + Delta cal. auto
# + Delta cal. man
# + Start probing
# + Move Z: 000.00
# + Test Z: ++
# + Accept
# + Abort
# + Bed probe
# + Dump parameters
### menu main ###
[menu __main]
type: list
name: Main
### menu tune ###
[menu __main __tune]
type: list
enable: {printer.idle_timeout.state == "Printing"}
name: Tune
[menu __main __tune __speed]
type: input
name: Speed: {'%3d' % (menu.input*100)}%
input: {printer.gcode_move.speed_factor}
input_min: 0.01
input_max: 5
input_step: 0.01
realtime: True
gcode:
M220 S{'%d' % (menu.input*100)}
[menu __main __tune __flow]
type: input
name: Flow: {'%3d' % (menu.input*100)}%
input: {printer.gcode_move.extrude_factor}
input_min: 0.01
input_max: 2
input_step: 0.01
realtime: True
gcode:
M221 S{'%d' % (menu.input*100)}
[menu __main __tune __offsetz]
type: input
name: Offset Z:{'%05.3f' % menu.input}
input: {printer.gcode_move.homing_origin.z}
input_min: -5
input_max: 5
input_step: 0.005
realtime: True
gcode:
SET_GCODE_OFFSET Z={'%.3f' % menu.input} MOVE=1
### menu octoprint ###
[menu __main __octoprint]
type: list
name: OctoPrint
[menu __main __octoprint __pause]
type: command
enable: {printer.idle_timeout.state == "Printing"}
name: Pause printing
gcode:
{action_respond_info('action:pause')}
[menu __main __octoprint __resume]
type: command
enable: {not printer.idle_timeout.state == "Printing"}
name: Resume printing
gcode:
{action_respond_info('action:resume')}
[menu __main __octoprint __abort]
type: command
enable: {printer.idle_timeout.state == "Printing"}
name: Abort printing
gcode:
{action_respond_info('action:cancel')}
### menu virtual sdcard ###
[menu __main __sdcard]
type: vsdlist
enable: {('virtual_sdcard' in printer)}
name: SD Card
[menu __main __sdcard __start]
type: command
enable: {('virtual_sdcard' in printer) and printer.virtual_sdcard.file_path and not printer.virtual_sdcard.is_active}
name: Start printing
gcode: M24
[menu __main __sdcard __resume]
type: command
enable: {('virtual_sdcard' in printer) and printer.print_stats.state == "paused"}
name: Resume printing
gcode:
{% if "pause_resume" in printer %}
RESUME
{% else %}
M24
{% endif %}
[menu __main __sdcard __pause]
type: command
enable: {('virtual_sdcard' in printer) and printer.print_stats.state == "printing"}
name: Pause printing
gcode:
{% if "pause_resume" in printer %}
PAUSE
{% else %}
M25
{% endif %}
[menu __main __sdcard __cancel]
type: command
enable: {('virtual_sdcard' in printer) and (printer.print_stats.state == "printing" or printer.print_stats.state == "paused")}
name: Cancel printing
gcode:
{% if 'pause_resume' in printer %}
CANCEL_PRINT
{% else %}
M25
M27
M26 S0
TURN_OFF_HEATERS
{% if printer.toolhead.position.z <= printer.toolhead.axis_maximum.z - 5 %}
G91
G0 Z5 F1000
G90
{% endif %}
{% endif %}
### menu control ###
[menu __main __control]
type: list
name: Control
[menu __main __control __home]
type: command
enable: {not printer.idle_timeout.state == "Printing"}
name: Home All
gcode: G28
[menu __main __control __homez]
type: command
enable: {not printer.idle_timeout.state == "Printing"}
name: Home Z
gcode: G28 Z
[menu __main __control __homexy]
type: command
enable: {not printer.idle_timeout.state == "Printing"}
name: Home X/Y
gcode: G28 X Y
[menu __main __control __disable]
type: command
name: Steppers off
gcode:
M84
M18
[menu __main __control __fanonoff]
type: input
enable: {'fan' in printer}
name: Fan: {'ON ' if menu.input else 'OFF'}
input: {printer.fan.speed}
input_min: 0
input_max: 1
input_step: 1
gcode:
M106 S{255 if menu.input else 0}
[menu __main __control __fanspeed]
type: input
enable: {'fan' in printer}
name: Fan speed: {'%3d' % (menu.input*100)}%
input: {printer.fan.speed}
input_min: 0
input_max: 1
input_step: 0.01
gcode:
M106 S{'%d' % (menu.input*255)}
[menu __main __control __caselightonoff]
type: input
enable: {'output_pin caselight' in printer}
name: Lights: {'ON ' if menu.input else 'OFF'}
input: {printer['output_pin caselight'].value}
input_min: 0
input_max: 1
input_step: 1
gcode:
SET_PIN PIN=caselight VALUE={1 if menu.input else 0}
[menu __main __control __caselightpwm]
type: input
enable: {'output_pin caselight' in printer}
name: Lights: {'%3d' % (menu.input*100)}%
input: {printer['output_pin caselight'].value}
input_min: 0.0
input_max: 1.0
input_step: 0.01
gcode:
SET_PIN PIN=caselight VALUE={menu.input}
### menu move 10mm ###
[menu __main __control __move_10mm]
type: list
enable: {not printer.idle_timeout.state == "Printing"}
name: Move 10mm
[menu __main __control __move_10mm __axis_x]
type: input
name: Move X:{'%05.1f' % menu.input}
input: {printer.gcode_move.gcode_position.x}
input_min: {printer.toolhead.axis_minimum.x}
input_max: {printer.toolhead.axis_maximum.x}
input_step: 10.0
gcode:
SAVE_GCODE_STATE NAME=__move__axis
G90
G1 X{menu.input}
RESTORE_GCODE_STATE NAME=__move__axis
[menu __main __control __move_10mm __axis_y]
type: input
name: Move Y:{'%05.1f' % menu.input}
input: {printer.gcode_move.gcode_position.y}
input_min: {printer.toolhead.axis_minimum.y}
input_max: {printer.toolhead.axis_maximum.y}
input_step: 10.0
gcode:
SAVE_GCODE_STATE NAME=__move__axis
G90
G1 Y{menu.input}
RESTORE_GCODE_STATE NAME=__move__axis
[menu __main __control __move_10mm __axis_z]
type: input
enable: {not printer.idle_timeout.state == "Printing"}
name: Move Z:{'%05.1f' % menu.input}
input: {printer.gcode_move.gcode_position.z}
input_min: 0
input_max: {printer.toolhead.axis_maximum.z}
input_step: 10.0
gcode:
SAVE_GCODE_STATE NAME=__move__axis
G90
G1 Z{menu.input}
RESTORE_GCODE_STATE NAME=__move__axis
[menu __main __control __move_10mm __axis_e]
type: input
enable: {not printer.idle_timeout.state == "Printing"}
name: Move E:{'%+06.1f' % menu.input}
input: 0
input_min: -{printer.configfile.config.extruder.max_extrude_only_distance|default(50)}
input_max: {printer.configfile.config.extruder.max_extrude_only_distance|default(50)}
input_step: 10.0
gcode:
SAVE_GCODE_STATE NAME=__move__axis
M83
G1 E{menu.input} F240
RESTORE_GCODE_STATE NAME=__move__axis
### menu move 1mm ###
[menu __main __control __move_1mm]
type: list
enable: {not printer.idle_timeout.state == "Printing"}
name: Move 1mm
[menu __main __control __move_1mm __axis_x]
type: input
name: Move X:{'%05.1f' % menu.input}
input: {printer.gcode_move.gcode_position.x}
input_min: {printer.toolhead.axis_minimum.x}
input_max: {printer.toolhead.axis_maximum.x}
input_step: 1.0
gcode:
SAVE_GCODE_STATE NAME=__move__axis
G90
G1 X{menu.input}
RESTORE_GCODE_STATE NAME=__move__axis
[menu __main __control __move_1mm __axis_y]
type: input
name: Move Y:{'%05.1f' % menu.input}
input: {printer.gcode_move.gcode_position.y}
input_min: {printer.toolhead.axis_minimum.y}
input_max: {printer.toolhead.axis_maximum.y}
input_step: 1.0
gcode:
SAVE_GCODE_STATE NAME=__move__axis
G90
G1 Y{menu.input}
RESTORE_GCODE_STATE NAME=__move__axis
[menu __main __control __move_1mm __axis_z]
type: input
enable: {not printer.idle_timeout.state == "Printing"}
name: Move Z:{'%05.1f' % menu.input}
input: {printer.gcode_move.gcode_position.z}
input_min: 0
input_max: {printer.toolhead.axis_maximum.z}
input_step: 1.0
gcode:
SAVE_GCODE_STATE NAME=__move__axis
G90
G1 Z{menu.input}
RESTORE_GCODE_STATE NAME=__move__axis
[menu __main __control __move_1mm __axis_e]
type: input
enable: {not printer.idle_timeout.state == "Printing"}
name: Move E:{'%+06.1f' % menu.input}
input: 0
input_min: -{printer.configfile.config.extruder.max_extrude_only_distance|default(50)}
input_max: {printer.configfile.config.extruder.max_extrude_only_distance|default(50)}
input_step: 1.0
gcode:
SAVE_GCODE_STATE NAME=__move__axis
M83
G1 E{menu.input} F240
RESTORE_GCODE_STATE NAME=__move__axis
### menu move 0.1mm ###
[menu __main __control __move_01mm]
type: list
enable: {not printer.idle_timeout.state == "Printing"}
name: Move 0.1mm
[menu __main __control __move_01mm __axis_x]
type: input
name: Move X:{'%05.1f' % menu.input}
input: {printer.gcode_move.gcode_position.x}
input_min: {printer.toolhead.axis_minimum.x}
input_max: {printer.toolhead.axis_maximum.x}
input_step: 0.1
gcode:
SAVE_GCODE_STATE NAME=__move__axis
G90
G1 X{menu.input}
RESTORE_GCODE_STATE NAME=__move__axis
[menu __main __control __move_01mm __axis_y]
type: input
name: Move Y:{'%05.1f' % menu.input}
input: {printer.gcode_move.gcode_position.y}
input_min: {printer.toolhead.axis_minimum.y}
input_max: {printer.toolhead.axis_maximum.y}
input_step: 0.1
gcode:
SAVE_GCODE_STATE NAME=__move__axis
G90
G1 Y{menu.input}
RESTORE_GCODE_STATE NAME=__move__axis
[menu __main __control __move_01mm __axis_z]
type: input
enable: {not printer.idle_timeout.state == "Printing"}
name: Move Z:{'%05.1f' % menu.input}
input: {printer.gcode_move.gcode_position.z}
input_min: 0
input_max: {printer.toolhead.axis_maximum.z}
input_step: 0.1
gcode:
SAVE_GCODE_STATE NAME=__move__axis
G90
G1 Z{menu.input}
RESTORE_GCODE_STATE NAME=__move__axis
[menu __main __control __move_01mm __axis_e]
type: input
enable: {not printer.idle_timeout.state == "Printing"}
name: Move E:{'%+06.1f' % menu.input}
input: 0
input_min: -{printer.configfile.config.extruder.max_extrude_only_distance|default(50)}
input_max: {printer.configfile.config.extruder.max_extrude_only_distance|default(50)}
input_step: 0.1
gcode:
SAVE_GCODE_STATE NAME=__move__axis
M83
G1 E{menu.input} F240
RESTORE_GCODE_STATE NAME=__move__axis
### menu temperature ###
[menu __main __temp]
type: list
name: Temperature
[menu __main __temp __hotend0_target]
type: input
enable: {('extruder' in printer) and ('extruder' in printer.heaters.available_heaters)}
name: {"Ex0:%3.0f (%4.0f)" % (menu.input, printer.extruder.temperature)}
input: {printer.extruder.target}
input_min: 0
input_max: {printer.configfile.config.extruder.max_temp}
input_step: 1
gcode: M104 T0 S{'%.0f' % menu.input}
[menu __main __temp __hotend1_target]
type: input
enable: {('extruder1' in printer) and ('extruder1' in printer.heaters.available_heaters)}
name: {"Ex1:%3.0f (%4.0f)" % (menu.input, printer.extruder1.temperature)}
input: {printer.extruder1.target}
input_min: 0
input_max: {printer.configfile.config.extruder1.max_temp}
input_step: 1
gcode: M104 T1 S{'%.0f' % menu.input}
[menu __main __temp __hotbed_target]
type: input
enable: {'heater_bed' in printer}
name: {"Bed:%3.0f (%4.0f)" % (menu.input, printer.heater_bed.temperature)}
input: {printer.heater_bed.target}
input_min: 0
input_max: {printer.configfile.config.heater_bed.max_temp}
input_step: 1
gcode: M140 S{'%.0f' % menu.input}
[menu __main __temp __preheat_pla]
type: list
name: Preheat PLA
[menu __main __temp __preheat_pla __all]
type: command
enable: {('extruder' in printer) and ('heater_bed' in printer)}
name: Preheat all
gcode:
M140 S60
M104 S200
[menu __main __temp __preheat_pla __hotend]
type: command
enable: {'extruder' in printer}
name: Preheat hotend
gcode: M104 S200
[menu __main __temp __preheat_pla __hotbed]
type: command
enable: {'heater_bed' in printer}
name: Preheat hotbed
gcode: M140 S60
[menu __main __temp __preheat_abs]
type: list
name: Preheat ABS
[menu __main __temp __preheat_abs __all]
type: command
enable: {('extruder' in printer) and ('heater_bed' in printer)}
name: Preheat all
gcode:
M140 S110
M104 S245
[menu __main __temp __preheat_abs __hotend]
type: command
enable: {'extruder' in printer}
name: Preheat hotend
gcode: M104 S245
[menu __main __temp __preheat_abs __hotbed]
type: command
enable: {'heater_bed' in printer}
name: Preheat hotbed
gcode: M140 S110
[menu __main __temp __cooldown]
type: list
name: Cooldown
[menu __main __temp __cooldown __all]
type: command
enable: {('extruder' in printer) and ('heater_bed' in printer)}
name: Cooldown all
gcode:
M104 S0
M140 S0
[menu __main __temp __cooldown __hotend]
type: command
enable: {'extruder' in printer}
name: Cooldown hotend
gcode: M104 S0
[menu __main __temp __cooldown __hotbed]
type: command
enable: {'heater_bed' in printer}
name: Cooldown hotbed
gcode: M140 S0
### menu filament ###
[menu __main __filament]
type: list
name: Filament
[menu __main __filament __hotend0_target]
type: input
enable: {'extruder' in printer}
name: {"Ex0:%3.0f (%4.0f)" % (menu.input, printer.extruder.temperature)}
input: {printer.extruder.target}
input_min: 0
input_max: {printer.configfile.config.extruder.max_temp}
input_step: 1
gcode: M104 T0 S{'%.0f' % menu.input}
[menu __main __filament __loadf]
type: command
name: Load Fil. fast
gcode:
SAVE_GCODE_STATE NAME=__filament__load
M83
G1 E50 F960
RESTORE_GCODE_STATE NAME=__filament__load
[menu __main __filament __loads]
type: command
name: Load Fil. slow
gcode:
SAVE_GCODE_STATE NAME=__filament__load
M83
G1 E50 F240
RESTORE_GCODE_STATE NAME=__filament__load
[menu __main __filament __unloadf]
type: command
name: Unload Fil.fast
gcode:
SAVE_GCODE_STATE NAME=__filament__load
M83
G1 E-50 F960
RESTORE_GCODE_STATE NAME=__filament__load
[menu __main __filament __unloads]
type: command
name: Unload Fil.slow
gcode:
SAVE_GCODE_STATE NAME=__filament__load
M83
G1 E-50 F240
RESTORE_GCODE_STATE NAME=__filament__load
[menu __main __filament __feed]
type: input
name: Feed: {'%.1f' % menu.input}
input: 5
input_step: 0.1
gcode:
SAVE_GCODE_STATE NAME=__filament__load
M83
G1 E{'%.1f' % menu.input} F60
RESTORE_GCODE_STATE NAME=__filament__load
### menu setup ###
[menu __main __setup]
type: list
enable: {not printer.idle_timeout.state == "Printing"}
name: Setup
[menu __main __setup __save_config]
type: command
name: Save config
gcode: SAVE_CONFIG
[menu __main __setup __restart]
type: list
name: Restart
[menu __main __setup __restart __host_restart]
type: command
enable: {not printer.idle_timeout.state == "Printing"}
name: Restart host
gcode: RESTART
[menu __main __setup __restart __firmware_restart]
type: command
enable: {not printer.idle_timeout.state == "Printing"}
name: Restart FW
gcode: FIRMWARE_RESTART
[menu __main __setup __tuning]
type: list
name: PID tuning
[menu __main __setup __tuning __hotend_pid_tuning]
type: command
enable: {(not printer.idle_timeout.state == "Printing") and ('extruder' in printer)}
name: Tune Hotend PID
gcode: PID_CALIBRATE HEATER=extruder TARGET=210 WRITE_FILE=1
[menu __main __setup __tuning __hotbed_pid_tuning]
type: command
enable: {(not printer.idle_timeout.state == "Printing") and ('heater_bed' in printer)}
name: Tune Hotbed PID
gcode: PID_CALIBRATE HEATER=heater_bed TARGET=60 WRITE_FILE=1
[menu __main __setup __calib]
type: list
name: Calibration
[menu __main __setup __calib __delta_calib_auto]
type: command
enable: {not printer.idle_timeout.state == "Printing"}
name: Delta cal. auto
gcode:
G28
DELTA_CALIBRATE
[menu __main __setup __calib __delta_calib_man]
type: list
enable: {not printer.idle_timeout.state == "Printing"}
name: Delta cal. man
[menu __main __setup __calib __bedprobe]
type: command
enable: {not printer.idle_timeout.state == "Printing"}
name: Bed probe
gcode: PROBE
[menu __main __setup __calib __delta_calib_man __start]
type: command
name: Start probing
gcode:
G28
DELTA_CALIBRATE METHOD=manual
[menu __main __setup __calib __delta_calib_man __move_z]
type: input
name: Move Z: {'%03.2f' % menu.input}
input: {printer.gcode_move.gcode_position.z}
input_step: 1
realtime: True
gcode:
{%- if menu.event == 'change' -%}
G1 Z{'%.2f' % menu.input}
{%- elif menu.event == 'long_click' -%}
G1 Z{'%.2f' % menu.input}
SAVE_GCODE_STATE NAME=__move__axis
G91
G1 Z2
G1 Z-2
RESTORE_GCODE_STATE NAME=__move__axis
{%- endif -%}
[menu __main __setup __calib __delta_calib_man __test_z]
type: input
name: Test Z: {['++','+','+.01','+.05','+.1','+.5','-.5','-.1','-.05','-.01','-','--'][menu.input|int]}
input: 6
input_min: 0
input_max: 11
input_step: 1
gcode:
{%- if menu.event == 'long_click' -%}
TESTZ Z={['++','+','+.01','+.05','+.1','+.5','-.5','-.1','-.05','-.01','-','--'][menu.input|int]}
{%- endif -%}
[menu __main __setup __calib __delta_calib_man __accept]
type: command
name: Accept
gcode: ACCEPT
[menu __main __setup __calib __delta_calib_man __abort]
type: command
name: Abort
gcode: ABORT
[menu __main __setup __dump]
type: command
name: Dump parameters
gcode:
{% for name1 in printer %}
{% for name2 in printer[name1] %}
{ action_respond_info("printer['%s'].%s = %s"
% (name1, name2, printer[name1][name2])) }
{% else %}
{ action_respond_info("printer['%s'] = %s" % (name1, printer[name1])) }
{% endfor %}
{% endfor %}

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
# Support for menu button press tracking
#
# Copyright (C) 2018 Janar Sööt <janar.soot@gmail.com>
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
LONG_PRESS_DURATION = 0.800
TIMER_DELAY = .200
class MenuKeys:
def __init__(self, config, callback):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.callback = callback
buttons = self.printer.load_object(config, "buttons")
# Register rotary encoder
encoder_pins = config.get('encoder_pins', None)
encoder_steps_per_detent = config.getchoice('encoder_steps_per_detent',
{2: 2, 4: 4}, 4)
if encoder_pins is not None:
try:
pin1, pin2 = encoder_pins.split(',')
except:
raise config.error("Unable to parse encoder_pins")
buttons.register_rotary_encoder(pin1.strip(), pin2.strip(),
self.encoder_cw_callback,
self.encoder_ccw_callback,
encoder_steps_per_detent)
self.encoder_fast_rate = config.getfloat('encoder_fast_rate',
.030, above=0.)
self.last_encoder_cw_eventtime = 0
self.last_encoder_ccw_eventtime = 0
# Register click button
self.is_short_click = False
self.click_timer = self.reactor.register_timer(self.long_click_event)
self.register_button(config, 'click_pin', self.click_callback, False)
# Register other buttons
self.register_button(config, 'back_pin', self.back_callback)
self.register_button(config, 'up_pin', self.up_callback)
self.register_button(config, 'down_pin', self.down_callback)
self.register_button(config, 'kill_pin', self.kill_callback)
def register_button(self, config, name, callback, push_only=True):
pin = config.get(name, None)
if pin is None:
return
buttons = self.printer.lookup_object("buttons")
if config.get('analog_range_' + name, None) is None:
if push_only:
buttons.register_button_push(pin, callback)
else:
buttons.register_buttons([pin], callback)
return
amin, amax = config.getfloatlist('analog_range_' + name, count=2)
pullup = config.getfloat('analog_pullup_resistor', 4700., above=0.)
if push_only:
buttons.register_adc_button_push(pin, amin, amax, pullup, callback)
else:
buttons.register_adc_button(pin, amin, amax, pullup, callback)
# Rotary encoder callbacks
def encoder_cw_callback(self, eventtime):
fast_rate = ((eventtime - self.last_encoder_cw_eventtime)
<= self.encoder_fast_rate)
self.last_encoder_cw_eventtime = eventtime
if fast_rate:
self.callback('fast_up', eventtime)
else:
self.callback('up', eventtime)
def encoder_ccw_callback(self, eventtime):
fast_rate = ((eventtime - self.last_encoder_ccw_eventtime)
<= self.encoder_fast_rate)
self.last_encoder_ccw_eventtime = eventtime
if fast_rate:
self.callback('fast_down', eventtime)
else:
self.callback('down', eventtime)
# Click handling
def long_click_event(self, eventtime):
self.is_short_click = False
self.callback('long_click', eventtime)
return self.reactor.NEVER
def click_callback(self, eventtime, state):
if state:
self.is_short_click = True
self.reactor.update_timer(self.click_timer,
eventtime + LONG_PRESS_DURATION)
elif self.is_short_click:
self.reactor.update_timer(self.click_timer, self.reactor.NEVER)
self.callback('click', eventtime)
# Other button callbacks
def back_callback(self, eventtime):
self.callback('back', eventtime)
def up_callback(self, eventtime):
self.callback('up', eventtime)
def down_callback(self, eventtime):
self.callback('down', eventtime)
def kill_callback(self, eventtime):
self.printer.invoke_shutdown("Shutdown due to kill button!")

Binary file not shown.

View File

@@ -0,0 +1,257 @@
# Support for ST7920 (128x64 graphics) LCD displays
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from .. import bus
from . import font8x14
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
# Spec says 72us, but faster is possible in practice
ST7920_CMD_DELAY = .000020
ST7920_SYNC_DELAY = .000045
TextGlyphs = { 'right_arrow': b'\x1a' }
CharGlyphs = { 'degrees': bytearray(font8x14.VGA_FONT[0xf8]) }
class DisplayBase:
def __init__(self):
# framebuffers
self.text_framebuffer = bytearray(b' '*64)
self.glyph_framebuffer = bytearray(128)
self.graphics_framebuffers = [bytearray(32) for i in range(32)]
self.all_framebuffers = [
# Text framebuffer
(self.text_framebuffer, bytearray(b'~'*64), 0x80),
# Glyph framebuffer
(self.glyph_framebuffer, bytearray(b'~'*128), 0x40),
# Graphics framebuffers
] + [(self.graphics_framebuffers[i], bytearray(b'~'*32), i)
for i in range(32)]
self.cached_glyphs = {}
self.icons = {}
def flush(self):
# Find all differences in the framebuffers and send them to the chip
for new_data, old_data, fb_id in self.all_framebuffers:
if new_data == old_data:
continue
# Find the position of all changed bytes in this framebuffer
diffs = [[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data))
if n != o]
# Batch together changes that are close to each other
for i in range(len(diffs)-2, -1, -1):
pos, count = diffs[i]
nextpos, nextcount = diffs[i+1]
if pos + 5 >= nextpos and nextcount < 16:
diffs[i][1] = nextcount + (nextpos - pos)
del diffs[i+1]
# Transmit changes
for pos, count in diffs:
count += pos & 0x01
count += count & 0x01
pos = pos & ~0x01
chip_pos = pos >> 1
if fb_id < 0x40:
# Graphics framebuffer update
self.send([0x80 + fb_id, 0x80 + chip_pos], is_extended=True)
else:
self.send([fb_id + chip_pos])
self.send(new_data[pos:pos+count], is_data=True)
old_data[:] = new_data
def init(self):
cmds = [0x24, # Enter extended mode
0x40, # Clear vertical scroll address
0x02, # Enable CGRAM access
0x26, # Enable graphics
0x22, # Leave extended mode
0x02, # Home the display
0x06, # Set positive update direction
0x0c] # Enable display and hide cursor
self.send(cmds)
self.flush()
def cache_glyph(self, glyph_name, base_glyph_name, glyph_id):
icon = self.icons.get(glyph_name)
base_icon = self.icons.get(base_glyph_name)
if icon is None or base_icon is None:
return
all_bits = zip(icon[0], icon[1], base_icon[0], base_icon[1])
for i, (ic1, ic2, b1, b2) in enumerate(all_bits):
x1, x2 = ic1 ^ b1, ic2 ^ b2
pos = glyph_id*32 + i*2
self.glyph_framebuffer[pos:pos+2] = [x1, x2]
self.all_framebuffers[1][1][pos:pos+2] = [x1 ^ 1, x2 ^ 1]
self.cached_glyphs[glyph_name] = (base_glyph_name, (0, glyph_id*2))
def set_glyphs(self, glyphs):
for glyph_name, glyph_data in glyphs.items():
icon = glyph_data.get('icon16x16')
if icon is not None:
self.icons[glyph_name] = icon
# Setup animated glyphs
self.cache_glyph('fan2', 'fan1', 0)
self.cache_glyph('bed_heat2', 'bed_heat1', 1)
def write_text(self, x, y, data):
if x + len(data) > 16:
data = data[:16 - min(x, 16)]
pos = [0, 32, 16, 48][y] + x
self.text_framebuffer[pos:pos+len(data)] = data
def write_graphics(self, x, y, data):
if x >= 16 or y >= 4 or len(data) != 16:
return
gfx_fb = y * 16
if gfx_fb >= 32:
gfx_fb -= 32
x += 16
for i, bits in enumerate(data):
self.graphics_framebuffers[gfx_fb + i][x] = bits
def write_glyph(self, x, y, glyph_name):
glyph_id = self.cached_glyphs.get(glyph_name)
if glyph_id is not None and x & 1 == 0:
# Render cached icon using character generator
glyph_name = glyph_id[0]
self.write_text(x, y, glyph_id[1])
icon = self.icons.get(glyph_name)
if icon is not None:
# Draw icon in graphics mode
self.write_graphics(x, y, icon[0])
self.write_graphics(x + 1, y, icon[1])
return 2
char = TextGlyphs.get(glyph_name)
if char is not None:
# Draw character
self.write_text(x, y, char)
return 1
font = CharGlyphs.get(glyph_name)
if font is not None:
# Draw single width character
self.write_graphics(x, y, font)
return 1
return 0
def clear(self):
self.text_framebuffer[:] = b' '*64
zeros = bytearray(32)
for gfb in self.graphics_framebuffers:
gfb[:] = zeros
def get_dimensions(self):
return (16, 4)
# Display driver for stock ST7920 displays
class ST7920(DisplayBase):
def __init__(self, config):
printer = config.get_printer()
# pin config
ppins = printer.lookup_object('pins')
pins = [ppins.lookup_pin(config.get(name + '_pin'))
for name in ['cs', 'sclk', 'sid']]
mcu = None
for pin_params in pins:
if mcu is not None and pin_params['chip'] != mcu:
raise ppins.error("st7920 all pins must be on same mcu")
mcu = pin_params['chip']
self.pins = [pin_params['pin'] for pin_params in pins]
# prepare send functions
self.mcu = mcu
self.oid = self.mcu.create_oid()
self.mcu.register_config_callback(self.build_config)
self.send_data_cmd = self.send_cmds_cmd = None
self.is_extended = False
# init display base
DisplayBase.__init__(self)
def build_config(self):
# configure send functions
self.mcu.add_config_cmd(
"config_st7920 oid=%u cs_pin=%s sclk_pin=%s sid_pin=%s"
" sync_delay_ticks=%d cmd_delay_ticks=%d" % (
self.oid, self.pins[0], self.pins[1], self.pins[2],
self.mcu.seconds_to_clock(ST7920_SYNC_DELAY),
self.mcu.seconds_to_clock(ST7920_CMD_DELAY)))
cmd_queue = self.mcu.alloc_command_queue()
self.send_cmds_cmd = self.mcu.lookup_command(
"st7920_send_cmds oid=%c cmds=%*s", cq=cmd_queue)
self.send_data_cmd = self.mcu.lookup_command(
"st7920_send_data oid=%c data=%*s", cq=cmd_queue)
def send(self, cmds, is_data=False, is_extended=False):
cmd_type = self.send_cmds_cmd
if is_data:
cmd_type = self.send_data_cmd
elif self.is_extended != is_extended:
add_cmd = 0x22
if is_extended:
add_cmd = 0x26
cmds = [add_cmd] + cmds
self.is_extended = is_extended
cmd_type.send([self.oid, cmds], reqclock=BACKGROUND_PRIORITY_CLOCK)
#logging.debug("st7920 %d %s", is_data, repr(cmds))
# Helper code for toggling the en pin on startup
class EnableHelper:
def __init__(self, pin_desc, spi):
self.en_pin = bus.MCU_bus_digital_out(spi.get_mcu(), pin_desc,
spi.get_command_queue())
def init(self):
mcu = self.en_pin.get_mcu()
curtime = mcu.get_printer().get_reactor().monotonic()
print_time = mcu.estimated_print_time(curtime)
# Toggle enable pin
minclock = mcu.print_time_to_clock(print_time + .100)
self.en_pin.update_digital_out(0, minclock=minclock)
minclock = mcu.print_time_to_clock(print_time + .200)
self.en_pin.update_digital_out(1, minclock=minclock)
# Force a delay to any subsequent commands on the command queue
minclock = mcu.print_time_to_clock(print_time + .300)
self.en_pin.update_digital_out(1, minclock=minclock)
# Display driver for displays that emulate the ST7920 in software.
# These displays rely on the CS pin to be toggled in order to initialize the
# SPI correctly. This display driver uses a software SPI with an unused pin
# as the MISO pin.
class EmulatedST7920(DisplayBase):
def __init__(self, config):
# create software spi
ppins = config.get_printer().lookup_object('pins')
sw_pin_names = ['spi_software_%s_pin' % (name,)
for name in ['miso', 'mosi', 'sclk']]
sw_pin_params = [ppins.lookup_pin(config.get(name), share_type=name)
for name in sw_pin_names]
mcu = None
for pin_params in sw_pin_params:
if mcu is not None and pin_params['chip'] != mcu:
raise ppins.error("%s: spi pins must be on same mcu" % (
config.get_name(),))
mcu = pin_params['chip']
sw_pins = tuple([pin_params['pin'] for pin_params in sw_pin_params])
speed = config.getint('spi_speed', 1000000, minval=100000)
self.spi = bus.MCU_SPI(mcu, None, None, 0, speed, sw_pins)
# create enable helper
self.en_helper = EnableHelper(config.get("en_pin"), self.spi)
self.en_set = False
# init display base
self.is_extended = False
DisplayBase.__init__(self)
def send(self, cmds, is_data=False, is_extended=False):
# setup sync byte and check for exten mode switch
sync_byte = 0xfa
if not is_data:
sync_byte = 0xf8
if self.is_extended != is_extended:
add_cmd = 0x22
if is_extended:
add_cmd = 0x26
cmds = [add_cmd] + cmds
self.is_extended = is_extended
# copy data to ST7920 data format
spi_data = [0] * (2 * len(cmds) + 1)
spi_data[0] = sync_byte
i = 1
for b in cmds:
spi_data[i] = b & 0xF0
spi_data[i + 1] = (b & 0x0F) << 4
i = i + 2
# check if enable pin has been set
if not self.en_set:
self.en_helper.init()
self.en_set = True
# send data
self.spi.spi_send(spi_data, reqclock=BACKGROUND_PRIORITY_CLOCK)
#logging.debug("st7920 %s", repr(spi_data))

Binary file not shown.

View File

@@ -0,0 +1,240 @@
# Support for UC1701 (and similar) 128x64 graphics LCD displays
#
# Copyright (C) 2018-2019 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from .. import bus
from . import font8x14
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
TextGlyphs = { 'right_arrow': b'\x1a', 'degrees': b'\xf8' }
class DisplayBase:
def __init__(self, io, columns=128, x_offset=0):
self.send = io.send
# framebuffers
self.columns = columns
self.x_offset = x_offset
self.vram = [bytearray(self.columns) for i in range(8)]
self.all_framebuffers = [(self.vram[i], bytearray(b'~'*self.columns), i)
for i in range(8)]
# Cache fonts and icons in display byte order
self.font = [self._swizzle_bits(bytearray(c))
for c in font8x14.VGA_FONT]
self.icons = {}
def flush(self):
# Find all differences in the framebuffers and send them to the chip
for new_data, old_data, page in self.all_framebuffers:
if new_data == old_data:
continue
# Find the position of all changed bytes in this framebuffer
diffs = [[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data))
if n != o]
# Batch together changes that are close to each other
for i in range(len(diffs)-2, -1, -1):
pos, count = diffs[i]
nextpos, nextcount = diffs[i+1]
if pos + 5 >= nextpos and nextcount < 16:
diffs[i][1] = nextcount + (nextpos - pos)
del diffs[i+1]
# Transmit changes
for col_pos, count in diffs:
# Set Position registers
ra = 0xb0 | (page & 0x0F)
ca_msb = 0x10 | ((col_pos >> 4) & 0x0F)
ca_lsb = col_pos & 0x0F
self.send([ra, ca_msb, ca_lsb])
# Send Data
self.send(new_data[col_pos:col_pos+count], is_data=True)
old_data[:] = new_data
def _swizzle_bits(self, data):
# Convert from "rows of pixels" format to "columns of pixels"
top = bot = 0
for row in range(8):
spaced = (data[row] * 0x8040201008040201) & 0x8080808080808080
top |= spaced >> (7 - row)
spaced = (data[row + 8] * 0x8040201008040201) & 0x8080808080808080
bot |= spaced >> (7 - row)
bits_top = [(top >> s) & 0xff for s in range(0, 64, 8)]
bits_bot = [(bot >> s) & 0xff for s in range(0, 64, 8)]
return (bytearray(bits_top), bytearray(bits_bot))
def set_glyphs(self, glyphs):
for glyph_name, glyph_data in glyphs.items():
icon = glyph_data.get('icon16x16')
if icon is not None:
top1, bot1 = self._swizzle_bits(icon[0])
top2, bot2 = self._swizzle_bits(icon[1])
self.icons[glyph_name] = (top1 + top2, bot1 + bot2)
def write_text(self, x, y, data):
if x + len(data) > 16:
data = data[:16 - min(x, 16)]
pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1]
for c in bytearray(data):
bits_top, bits_bot = self.font[c]
page_top[pix_x:pix_x+8] = bits_top
page_bot[pix_x:pix_x+8] = bits_bot
pix_x += 8
def write_graphics(self, x, y, data):
if x >= 16 or y >= 4 or len(data) != 16:
return
bits_top, bits_bot = self._swizzle_bits(data)
pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1]
for i in range(8):
page_top[pix_x + i] ^= bits_top[i]
page_bot[pix_x + i] ^= bits_bot[i]
def write_glyph(self, x, y, glyph_name):
icon = self.icons.get(glyph_name)
if icon is not None and x < 15:
# Draw icon in graphics mode
pix_x = x * 8
pix_x += self.x_offset
page_idx = y * 2
self.vram[page_idx][pix_x:pix_x+16] = icon[0]
self.vram[page_idx + 1][pix_x:pix_x+16] = icon[1]
return 2
char = TextGlyphs.get(glyph_name)
if char is not None:
# Draw character
self.write_text(x, y, char)
return 1
return 0
def clear(self):
zeros = bytearray(self.columns)
for page in self.vram:
page[:] = zeros
def get_dimensions(self):
return (16, 4)
# IO wrapper for "4 wire" spi bus (spi bus with an extra data/control line)
class SPI4wire:
def __init__(self, config, data_pin_name):
self.spi = bus.MCU_SPI_from_config(config, 0, default_speed=10000000)
dc_pin = config.get(data_pin_name)
self.mcu_dc = bus.MCU_bus_digital_out(self.spi.get_mcu(), dc_pin,
self.spi.get_command_queue())
def send(self, cmds, is_data=False):
self.mcu_dc.update_digital_out(is_data,
reqclock=BACKGROUND_PRIORITY_CLOCK)
self.spi.spi_send(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
# IO wrapper for i2c bus
class I2C:
def __init__(self, config, default_addr):
self.i2c = bus.MCU_I2C_from_config(config, default_addr=default_addr,
default_speed=400000)
def send(self, cmds, is_data=False):
if is_data:
hdr = 0x40
else:
hdr = 0x00
cmds = bytearray(cmds)
cmds.insert(0, hdr)
self.i2c.i2c_write(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
# Helper code for toggling a reset pin on startup
class ResetHelper:
def __init__(self, pin_desc, io_bus):
self.mcu_reset = None
if pin_desc is None:
return
self.mcu_reset = bus.MCU_bus_digital_out(io_bus.get_mcu(), pin_desc,
io_bus.get_command_queue())
def init(self):
if self.mcu_reset is None:
return
mcu = self.mcu_reset.get_mcu()
curtime = mcu.get_printer().get_reactor().monotonic()
print_time = mcu.estimated_print_time(curtime)
# Toggle reset
minclock = mcu.print_time_to_clock(print_time + .100)
self.mcu_reset.update_digital_out(0, minclock=minclock)
minclock = mcu.print_time_to_clock(print_time + .200)
self.mcu_reset.update_digital_out(1, minclock=minclock)
# Force a delay to any subsequent commands on the command queue
minclock = mcu.print_time_to_clock(print_time + .300)
self.mcu_reset.update_digital_out(1, minclock=minclock)
# The UC1701 is a "4-wire" SPI display device
class UC1701(DisplayBase):
def __init__(self, config):
io = SPI4wire(config, "a0_pin")
DisplayBase.__init__(self, io)
self.contrast = config.getint('contrast', 40, minval=0, maxval=63)
self.reset = ResetHelper(config.get("rst_pin", None), io.spi)
def init(self):
self.reset.init()
init_cmds = [0xE2, # System reset
0x40, # Set display to start at line 0
0xA0, # Set SEG direction
0xC8, # Set COM Direction
0xA2, # Set Bias = 1/9
0x2C, # Boost ON
0x2E, # Voltage regulator on
0x2F, # Voltage follower on
0xF8, # Set booster ratio
0x00, # Booster ratio value (4x)
0x23, # Set resistor ratio (3)
0x81, # Set Electronic Volume
self.contrast, # Electronic Volume value
0xAC, # Set static indicator off
0x00, # NOP
0xA6, # Disable Inverse
0xAF] # Set display enable
self.send(init_cmds)
self.send([0xA5]) # display all
self.send([0xA4]) # normal display
self.flush()
# The SSD1306 supports both i2c and "4-wire" spi
class SSD1306(DisplayBase):
def __init__(self, config, columns=128, x_offset=0):
cs_pin = config.get("cs_pin", None)
if cs_pin is None:
io = I2C(config, 60)
io_bus = io.i2c
else:
io = SPI4wire(config, "dc_pin")
io_bus = io.spi
self.reset = ResetHelper(config.get("reset_pin", None), io_bus)
DisplayBase.__init__(self, io, columns, x_offset)
self.contrast = config.getint('contrast', 239, minval=0, maxval=255)
self.vcomh = config.getint('vcomh', 0, minval=0, maxval=63)
self.invert = config.getboolean('invert', False)
def init(self):
self.reset.init()
init_cmds = [
0xAE, # Display off
0xD5, 0x80, # Set oscillator frequency
0xA8, 0x3f, # Set multiplex ratio
0xD3, 0x00, # Set display offset
0x40, # Set display start line
0x8D, 0x14, # Charge pump setting
0x20, 0x02, # Set Memory addressing mode
0xA1, # Set Segment re-map
0xC8, # Set COM output scan direction
0xDA, 0x12, # Set COM pins hardware configuration
0x81, self.contrast, # Set contrast control
0xD9, 0xA1, # Set pre-charge period
0xDB, self.vcomh, # Set VCOMH deselect level
0x2E, # Deactivate scroll
0xA4, # Output ram to display
0xA7 if self.invert else 0xA6, # Set normal/invert
0xAF, # Display on
]
self.send(init_cmds)
self.flush()
# the SH1106 is SSD1306 compatible with up to 132 columns
class SH1106(SSD1306):
def __init__(self, config):
x_offset = config.getint('x_offset', 0, minval=0, maxval=3)
SSD1306.__init__(self, config, 132, x_offset=x_offset)

Binary file not shown.

View File

@@ -0,0 +1,50 @@
# Module to handle M73 and M117 display status commands
#
# Copyright (C) 2018-2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
M73_TIMEOUT = 5.
class DisplayStatus:
def __init__(self, config):
self.printer = config.get_printer()
self.expire_progress = 0.
self.progress = self.message = None
# Register commands
gcode = self.printer.lookup_object('gcode')
gcode.register_command('M73', self.cmd_M73)
gcode.register_command('M117', self.cmd_M117)
gcode.register_command(
'SET_DISPLAY_TEXT', self.cmd_SET_DISPLAY_TEXT,
desc=self.cmd_SET_DISPLAY_TEXT_help)
def get_status(self, eventtime):
progress = self.progress
if progress is not None and eventtime > self.expire_progress:
idle_timeout = self.printer.lookup_object('idle_timeout')
idle_timeout_info = idle_timeout.get_status(eventtime)
if idle_timeout_info['state'] != "Printing":
self.progress = progress = None
if progress is None:
progress = 0.
sdcard = self.printer.lookup_object('virtual_sdcard', None)
if sdcard is not None:
progress = sdcard.get_status(eventtime)['progress']
return { 'progress': progress, 'message': self.message }
def cmd_M73(self, gcmd):
progress = gcmd.get_float('P', None)
if progress is not None:
progress = progress / 100.
self.progress = min(1., max(0., progress))
curtime = self.printer.get_reactor().monotonic()
self.expire_progress = curtime + M73_TIMEOUT
def cmd_M117(self, gcmd):
msg = gcmd.get_raw_command_parameters() or None
self.message = msg
cmd_SET_DISPLAY_TEXT_help = "Set or clear the display message"
def cmd_SET_DISPLAY_TEXT(self, gcmd):
self.message = gcmd.get("MSG", None)
def load_config(config):
return DisplayStatus(config)

Binary file not shown.

58
klippy/extras/dotstar.py Normal file
View File

@@ -0,0 +1,58 @@
# Support for "dotstar" leds
#
# Copyright (C) 2019-2022 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import bus
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
class PrinterDotstar:
def __init__(self, config):
self.printer = printer = config.get_printer()
name = config.get_name().split()[1]
# Configure a software spi bus
ppins = printer.lookup_object('pins')
data_pin_params = ppins.lookup_pin(config.get('data_pin'))
clock_pin_params = ppins.lookup_pin(config.get('clock_pin'))
mcu = data_pin_params['chip']
if mcu is not clock_pin_params['chip']:
raise config.error("Dotstar pins must be on same mcu")
sw_spi_pins = (data_pin_params['pin'], data_pin_params['pin'],
clock_pin_params['pin'])
self.spi = bus.MCU_SPI(mcu, None, None, 0, 500000, sw_spi_pins)
# Initialize color data
self.chain_count = config.getint('chain_count', 1, minval=1)
pled = printer.load_object(config, "led")
self.led_helper = pled.setup_helper(config, self.update_leds,
self.chain_count)
self.prev_data = None
# Register commands
printer.register_event_handler("klippy:connect", self.handle_connect)
def handle_connect(self):
self.update_leds(self.led_helper.get_status()['color_data'], None)
def update_leds(self, led_state, print_time):
if led_state == self.prev_data:
return
self.prev_data = led_state
# Build data to send
data = [0] * ((len(led_state) + 2) * 4)
for i, (red, green, blue, white) in enumerate(led_state):
idx = (i + 1) * 4
data[idx] = 0xff
data[idx+1] = int(blue * 255. + .5)
data[idx+2] = int(green * 255. + .5)
data[idx+3] = int(red * 255. + .5)
data[-4] = data[-3] = data[-2] = data[-1] = 0xff
# Transmit update
minclock = 0
if print_time is not None:
minclock = self.spi.get_mcu().print_time_to_clock(print_time)
for d in [data[i:i+20] for i in range(0, len(data), 20)]:
self.spi.spi_send(d, minclock=minclock,
reqclock=BACKGROUND_PRIORITY_CLOCK)
def get_status(self, eventtime):
return self.led_helper.get_status(eventtime)
def load_config_prefix(config):
return PrinterDotstar(config)

80
klippy/extras/ds18b20.py Normal file
View File

@@ -0,0 +1,80 @@
# Support for 1-wire based temperature sensors
#
# Copyright (C) 2020 Alan Lord <alanslists@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import mcu
DS18_REPORT_TIME = 3.0
# Temperature can be sampled at any time but conversion time is ~750ms, so
# setting the time too low will not make the reports come faster.
DS18_MIN_REPORT_TIME = 1.0
DS18_MAX_CONSECUTIVE_ERRORS = 4
class DS18B20:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.sensor_id = bytearray(config.get("serial_no").encode())
self.temp = self.min_temp = self.max_temp = 0.0
self._report_clock = 0
self.report_time = config.getfloat(
'ds18_report_time',
DS18_REPORT_TIME,
minval=DS18_MIN_REPORT_TIME
)
self._mcu = mcu.get_printer_mcu(self.printer, config.get('sensor_mcu'))
self.oid = self._mcu.create_oid()
self._mcu.register_response(self._handle_ds18b20_response,
"ds18b20_result", self.oid)
self._mcu.register_config_callback(self._build_config)
def _build_config(self):
sid = "".join(["%02x" % (x,) for x in self.sensor_id])
self._mcu.add_config_cmd(
"config_ds18b20 oid=%d serial=%s max_error_count=%d"
% (self.oid, sid, DS18_MAX_CONSECUTIVE_ERRORS))
clock = self._mcu.get_query_slot(self.oid)
self._report_clock = self._mcu.seconds_to_clock(self.report_time)
self._mcu.add_config_cmd("query_ds18b20 oid=%d clock=%u rest_ticks=%u"
" min_value=%d max_value=%d" % (
self.oid, clock, self._report_clock,
self.min_temp * 1000, self.max_temp * 1000), is_init=True)
def _handle_ds18b20_response(self, params):
temp = params['value'] / 1000.0
if params["fault"]:
logging.info("ds18b20 reports fault %d (temp=%0.1f)",
params["fault"], temp)
return
next_clock = self._mcu.clock32_to_clock64(params['next_clock'])
last_read_clock = next_clock - self._report_clock
last_read_time = self._mcu.clock_to_print_time(last_read_clock)
self._callback(last_read_time, temp)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
def fault(self, msg):
self.printer.invoke_async_shutdown(msg)
def get_report_time_delta(self):
return self.report_time
def setup_callback(self, cb):
self._callback = cb
def get_status(self, eventtime):
return {
'temperature': round(self.temp, 2),
}
def load_config(config):
# Register sensor
pheaters = config.get_printer().load_object(config, "heaters")
pheaters.add_sensor_factory("DS18B20", DS18B20)

BIN
klippy/extras/ds18b20.pyc Normal file

Binary file not shown.

View File

@@ -0,0 +1,15 @@
# Tool to disable config checks for duplicate pins
#
# Copyright (C) 2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class PrinterDupPinOverride:
def __init__(self, config):
printer = config.get_printer()
ppins = printer.lookup_object('pins')
for pin_desc in config.getlist('pins'):
ppins.allow_multi_use_pin(pin_desc)
def load_config(config):
return PrinterDupPinOverride(config)

View File

@@ -0,0 +1,231 @@
# Endstop accuracy improvement via stepper phase tracking
#
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging
import stepper
TRINAMIC_DRIVERS = ["tmc2130", "tmc2208", "tmc2209", "tmc2240", "tmc2660", "tmc5160"]
# Calculate the trigger phase of a stepper motor
class PhaseCalc:
def __init__(self, printer, name, phases=None):
self.printer = printer
self.name = name
self.phases = phases
self.tmc_module = None
# Statistics tracking for ENDSTOP_PHASE_CALIBRATE
self.phase_history = self.last_phase = self.last_mcu_position = None
self.is_primary = self.stats_only = False
def lookup_tmc(self):
for driver in TRINAMIC_DRIVERS:
driver_name = "%s %s" % (driver, self.name)
module = self.printer.lookup_object(driver_name, None)
if module is not None:
self.tmc_module = module
if self.phases is None:
phase_offset, self.phases = module.get_phase_offset()
break
if self.phases is not None:
self.phase_history = [0] * self.phases
def convert_phase(self, driver_phase, driver_phases):
phases = self.phases
return (int(float(driver_phase) / driver_phases * phases + .5) % phases)
def calc_phase(self, stepper, trig_mcu_pos):
mcu_phase_offset = 0
if self.tmc_module is not None:
mcu_phase_offset, phases = self.tmc_module.get_phase_offset()
if mcu_phase_offset is None:
if self.printer.get_start_args().get('debugoutput') is None:
raise self.printer.command_error("Stepper %s phase unknown"
% (self.name,))
mcu_phase_offset = 0
phase = (trig_mcu_pos + mcu_phase_offset) % self.phases
self.phase_history[phase] += 1
self.last_phase = phase
self.last_mcu_position = trig_mcu_pos
return phase
# Adjusted endstop trigger positions
class EndstopPhase:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[1]
# Obtain step_distance and microsteps from stepper config section
sconfig = config.getsection(self.name)
rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig)
self.step_dist = rotation_dist / steps_per_rotation
self.phases = sconfig.getint("microsteps", note_valid=False) * 4
self.phase_calc = PhaseCalc(self.printer, self.name, self.phases)
# Register event handlers
self.printer.register_event_handler("klippy:connect",
self.phase_calc.lookup_tmc)
self.printer.register_event_handler("homing:home_rails_end",
self.handle_home_rails_end)
self.printer.load_object(config, "endstop_phase")
# Read config
self.endstop_phase = None
trigger_phase = config.get('trigger_phase', None)
if trigger_phase is not None:
p, ps = config.getintlist('trigger_phase', sep='/', count=2)
if p >= ps:
raise config.error("Invalid trigger_phase '%s'"
% (trigger_phase,))
self.endstop_phase = self.phase_calc.convert_phase(p, ps)
self.endstop_align_zero = config.getboolean('endstop_align_zero', False)
self.endstop_accuracy = config.getfloat('endstop_accuracy', None,
above=0.)
# Determine endstop accuracy
if self.endstop_accuracy is None:
self.endstop_phase_accuracy = self.phases//2 - 1
elif self.endstop_phase is not None:
self.endstop_phase_accuracy = int(
math.ceil(self.endstop_accuracy * .5 / self.step_dist))
else:
self.endstop_phase_accuracy = int(
math.ceil(self.endstop_accuracy / self.step_dist))
if self.endstop_phase_accuracy >= self.phases // 2:
raise config.error("Endstop for %s is not accurate enough for"
" stepper phase adjustment" % (self.name,))
if self.printer.get_start_args().get('debugoutput') is not None:
self.endstop_phase_accuracy = self.phases
def align_endstop(self, rail):
if not self.endstop_align_zero or self.endstop_phase is None:
return 0.
# Adjust the endstop position so 0.0 is always at a full step
microsteps = self.phases // 4
half_microsteps = microsteps // 2
phase_offset = (((self.endstop_phase + half_microsteps) % microsteps)
- half_microsteps) * self.step_dist
full_step = microsteps * self.step_dist
pe = rail.get_homing_info().position_endstop
return int(pe / full_step + .5) * full_step - pe + phase_offset
def get_homed_offset(self, stepper, trig_mcu_pos):
phase = self.phase_calc.calc_phase(stepper, trig_mcu_pos)
if self.endstop_phase is None:
logging.info("Setting %s endstop phase to %d", self.name, phase)
self.endstop_phase = phase
return 0.
delta = (phase - self.endstop_phase) % self.phases
if delta >= self.phases - self.endstop_phase_accuracy:
delta -= self.phases
elif delta > self.endstop_phase_accuracy:
raise self.printer.command_error(
"Endstop %s incorrect phase (got %d vs %d)" % (
self.name, phase, self.endstop_phase))
return delta * self.step_dist
def handle_home_rails_end(self, homing_state, rails):
for rail in rails:
stepper = rail.get_steppers()[0]
if stepper.get_name() == self.name:
trig_mcu_pos = homing_state.get_trigger_position(self.name)
align = self.align_endstop(rail)
offset = self.get_homed_offset(stepper, trig_mcu_pos)
homing_state.set_stepper_adjustment(self.name, align + offset)
return
# Support for ENDSTOP_PHASE_CALIBRATE command
class EndstopPhases:
def __init__(self, config):
self.printer = config.get_printer()
self.tracking = {}
# Register handlers
self.printer.register_event_handler("homing:home_rails_end",
self.handle_home_rails_end)
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command("ENDSTOP_PHASE_CALIBRATE",
self.cmd_ENDSTOP_PHASE_CALIBRATE,
desc=self.cmd_ENDSTOP_PHASE_CALIBRATE_help)
def update_stepper(self, stepper, trig_mcu_pos, is_primary):
stepper_name = stepper.get_name()
phase_calc = self.tracking.get(stepper_name)
if phase_calc is None:
# Check if stepper has an endstop_phase config section defined
mod_name = "endstop_phase %s" % (stepper_name,)
m = self.printer.lookup_object(mod_name, None)
if m is not None:
phase_calc = m.phase_calc
else:
# Create new PhaseCalc tracker
phase_calc = PhaseCalc(self.printer, stepper_name)
phase_calc.stats_only = True
phase_calc.lookup_tmc()
self.tracking[stepper_name] = phase_calc
if phase_calc.phase_history is None:
return
if is_primary:
phase_calc.is_primary = True
if phase_calc.stats_only:
phase_calc.calc_phase(stepper, trig_mcu_pos)
def handle_home_rails_end(self, homing_state, rails):
for rail in rails:
is_primary = True
for stepper in rail.get_steppers():
sname = stepper.get_name()
trig_mcu_pos = homing_state.get_trigger_position(sname)
self.update_stepper(stepper, trig_mcu_pos, is_primary)
is_primary = False
cmd_ENDSTOP_PHASE_CALIBRATE_help = "Calibrate stepper phase"
def cmd_ENDSTOP_PHASE_CALIBRATE(self, gcmd):
stepper_name = gcmd.get('STEPPER', None)
if stepper_name is None:
self.report_stats()
return
phase_calc = self.tracking.get(stepper_name)
if phase_calc is None or phase_calc.phase_history is None:
raise gcmd.error("Stats not available for stepper %s"
% (stepper_name,))
endstop_phase, phases = self.generate_stats(stepper_name, phase_calc)
if not phase_calc.is_primary:
return
configfile = self.printer.lookup_object('configfile')
section = 'endstop_phase %s' % (stepper_name,)
configfile.remove_section(section)
configfile.set(section, "trigger_phase",
"%s/%s" % (endstop_phase, phases))
gcmd.respond_info(
"The SAVE_CONFIG command will update the printer config\n"
"file with these parameters and restart the printer.")
def generate_stats(self, stepper_name, phase_calc):
phase_history = phase_calc.phase_history
wph = phase_history + phase_history
count = sum(phase_history)
phases = len(phase_history)
half_phases = phases // 2
res = []
for i in range(phases):
phase = i + half_phases
cost = sum([wph[j] * abs(j-phase) for j in range(i, i+phases)])
res.append((cost, phase))
res.sort()
best = res[0][1]
found = [j for j in range(best - half_phases, best + half_phases)
if wph[j]]
best_phase = best % phases
lo, hi = found[0] % phases, found[-1] % phases
self.gcode.respond_info("%s: trigger_phase=%d/%d (range %d to %d)"
% (stepper_name, best_phase, phases, lo, hi))
return best_phase, phases
def report_stats(self):
if not self.tracking:
self.gcode.respond_info(
"No steppers found. (Be sure to home at least once.)")
return
for stepper_name in sorted(self.tracking.keys()):
phase_calc = self.tracking[stepper_name]
if phase_calc is None or not phase_calc.is_primary:
continue
self.generate_stats(stepper_name, phase_calc)
def get_status(self, eventtime):
lh = { name: {'phase': pc.last_phase, 'phases': pc.phases,
'mcu_position': pc.last_mcu_position}
for name, pc in self.tracking.items()
if pc.phase_history is not None }
return { 'last_home': lh }
def load_config_prefix(config):
return EndstopPhase(config)
def load_config(config):
return EndstopPhases(config)

View File

@@ -0,0 +1,302 @@
# Exclude moves toward and inside objects
#
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
# Copyright (C) 2021 Troy Jacobson <troy.d.jacobson@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import json
class ExcludeObject:
def __init__(self, config):
self.printer = config.get_printer()
self.gcode = self.printer.lookup_object('gcode')
self.gcode_move = self.printer.load_object(config, 'gcode_move')
self.printer.register_event_handler('klippy:connect',
self._handle_connect)
self.printer.register_event_handler("virtual_sdcard:reset_file",
self._reset_file)
self.next_transform = None
self.last_position_extruded = [0., 0., 0., 0.]
self.last_position_excluded = [0., 0., 0., 0.]
self._reset_state()
self.gcode.register_command(
'EXCLUDE_OBJECT_START', self.cmd_EXCLUDE_OBJECT_START,
desc=self.cmd_EXCLUDE_OBJECT_START_help)
self.gcode.register_command(
'EXCLUDE_OBJECT_END', self.cmd_EXCLUDE_OBJECT_END,
desc=self.cmd_EXCLUDE_OBJECT_END_help)
self.gcode.register_command(
'EXCLUDE_OBJECT', self.cmd_EXCLUDE_OBJECT,
desc=self.cmd_EXCLUDE_OBJECT_help)
self.gcode.register_command(
'EXCLUDE_OBJECT_DEFINE', self.cmd_EXCLUDE_OBJECT_DEFINE,
desc=self.cmd_EXCLUDE_OBJECT_DEFINE_help)
def _register_transform(self):
if self.next_transform is None:
tuning_tower = self.printer.lookup_object('tuning_tower')
if tuning_tower.is_active():
logging.info('The ExcludeObject move transform is not being '
'loaded due to Tuning tower being Active')
return
self.next_transform = self.gcode_move.set_move_transform(self,
force=True)
self.extrusion_offsets = {}
self.max_position_extruded = 0
self.max_position_excluded = 0
self.extruder_adj = 0
self.initial_extrusion_moves = 5
self.last_position = [0., 0., 0., 0.]
self.get_position()
self.last_position_extruded[:] = self.last_position
self.last_position_excluded[:] = self.last_position
def _handle_connect(self):
self.toolhead = self.printer.lookup_object('toolhead')
def _unregister_transform(self):
if self.next_transform:
tuning_tower = self.printer.lookup_object('tuning_tower')
if tuning_tower.is_active():
logging.error('The Exclude Object move transform was not '
'unregistered because it is not at the head of the '
'transform chain.')
return
self.gcode_move.set_move_transform(self.next_transform, force=True)
self.next_transform = None
self.gcode_move.reset_last_position()
def _reset_state(self):
self.objects = []
self.excluded_objects = []
self.current_object = None
self.in_excluded_region = False
def _reset_file(self):
self._reset_state()
self._unregister_transform()
def _get_extrusion_offsets(self):
offset = self.extrusion_offsets.get(
self.toolhead.get_extruder().get_name())
if offset is None:
offset = [0., 0., 0., 0.]
self.extrusion_offsets[self.toolhead.get_extruder().get_name()] = \
offset
return offset
def get_position(self):
offset = self._get_extrusion_offsets()
pos = self.next_transform.get_position()
for i in range(4):
self.last_position[i] = pos[i] + offset[i]
return list(self.last_position)
def _normal_move(self, newpos, speed):
offset = self._get_extrusion_offsets()
if self.initial_extrusion_moves > 0 and \
self.last_position[3] != newpos[3]:
# Since the transform is not loaded until there is a request to
# exclude an object, the transform needs to track a few extrusions
# to get the state of the extruder
self.initial_extrusion_moves -= 1
self.last_position[:] = newpos
self.last_position_extruded[:] = self.last_position
self.max_position_extruded = max(self.max_position_extruded, newpos[3])
# These next few conditionals handle the moves immediately after leaving
# and excluded object. The toolhead is at the end of the last printed
# object and the gcode is at the end of the last excluded object.
#
# Ideally, there will be Z and E moves right away to adjust any offsets
# before moving away from the last position. Any remaining corrections
# will be made on the firs XY move.
if (offset[0] != 0 or offset[1] != 0) and \
(newpos[0] != self.last_position_excluded[0] or \
newpos[1] != self.last_position_excluded[1]):
offset[0] = 0
offset[1] = 0
offset[2] = 0
offset[3] += self.extruder_adj
self.extruder_adj = 0
if offset[2] != 0 and newpos[2] != self.last_position_excluded[2]:
offset[2] = 0
if self.extruder_adj != 0 and \
newpos[3] != self.last_position_excluded[3]:
offset[3] += self.extruder_adj
self.extruder_adj = 0
tx_pos = newpos[:]
for i in range(4):
tx_pos[i] = newpos[i] - offset[i]
self.next_transform.move(tx_pos, speed)
def _ignore_move(self, newpos, speed):
offset = self._get_extrusion_offsets()
for i in range(3):
offset[i] = newpos[i] - self.last_position_extruded[i]
offset[3] = offset[3] + newpos[3] - self.last_position[3]
self.last_position[:] = newpos
self.last_position_excluded[:] =self.last_position
self.max_position_excluded = max(self.max_position_excluded, newpos[3])
def _move_into_excluded_region(self, newpos, speed):
self.in_excluded_region = True
self._ignore_move(newpos, speed)
def _move_from_excluded_region(self, newpos, speed):
self.in_excluded_region = False
# This adjustment value is used to compensate for any retraction
# differences between the last object printed and excluded one.
self.extruder_adj = self.max_position_excluded \
- self.last_position_excluded[3] \
- (self.max_position_extruded - self.last_position_extruded[3])
self._normal_move(newpos, speed)
def _test_in_excluded_region(self):
# Inside cancelled object
return self.current_object in self.excluded_objects \
and self.initial_extrusion_moves == 0
def get_status(self, eventtime=None):
status = {
"objects": self.objects,
"excluded_objects": self.excluded_objects,
"current_object": self.current_object
}
return status
def move(self, newpos, speed):
move_in_excluded_region = self._test_in_excluded_region()
self.last_speed = speed
if move_in_excluded_region:
if self.in_excluded_region:
self._ignore_move(newpos, speed)
else:
self._move_into_excluded_region(newpos, speed)
else:
if self.in_excluded_region:
self._move_from_excluded_region(newpos, speed)
else:
self._normal_move(newpos, speed)
cmd_EXCLUDE_OBJECT_START_help = "Marks the beginning the current object" \
" as labeled"
def cmd_EXCLUDE_OBJECT_START(self, gcmd):
name = gcmd.get('NAME').upper()
if not any(obj["name"] == name for obj in self.objects):
self._add_object_definition({"name": name})
self.current_object = name
self.was_excluded_at_start = self._test_in_excluded_region()
cmd_EXCLUDE_OBJECT_END_help = "Marks the end the current object"
def cmd_EXCLUDE_OBJECT_END(self, gcmd):
if self.current_object == None and self.next_transform:
gcmd.respond_info("EXCLUDE_OBJECT_END called, but no object is"
" currently active")
return
name = gcmd.get('NAME', default=None)
if name != None and name.upper() != self.current_object:
gcmd.respond_info("EXCLUDE_OBJECT_END NAME=%s does not match the"
" current object NAME=%s" %
(name.upper(), self.current_object))
self.current_object = None
cmd_EXCLUDE_OBJECT_help = "Cancel moves inside a specified objects"
def cmd_EXCLUDE_OBJECT(self, gcmd):
reset = gcmd.get('RESET', None)
current = gcmd.get('CURRENT', None)
name = gcmd.get('NAME', '').upper()
if reset:
if name:
self._unexclude_object(name)
else:
self.excluded_objects = []
elif name:
if name.upper() not in self.excluded_objects:
self._exclude_object(name.upper())
elif current:
if not self.current_object:
gcmd.respond_error('There is no current object to cancel')
else:
self._exclude_object(self.current_object)
else:
self._list_excluded_objects(gcmd)
cmd_EXCLUDE_OBJECT_DEFINE_help = "Provides a summary of an object"
def cmd_EXCLUDE_OBJECT_DEFINE(self, gcmd):
reset = gcmd.get('RESET', None)
name = gcmd.get('NAME', '').upper()
if reset:
self._reset_file()
elif name:
parameters = gcmd.get_command_parameters().copy()
parameters.pop('NAME')
center = parameters.pop('CENTER', None)
polygon = parameters.pop('POLYGON', None)
obj = {"name": name.upper()}
obj.update(parameters)
if center != None:
obj['center'] = json.loads('[%s]' % center)
if polygon != None:
obj['polygon'] = json.loads(polygon)
self._add_object_definition(obj)
else:
self._list_objects(gcmd)
def _add_object_definition(self, definition):
self.objects = sorted(self.objects + [definition],
key=lambda o: o["name"])
def _exclude_object(self, name):
self._register_transform()
self.gcode.respond_info('Excluding object {}'.format(name.upper()))
if name not in self.excluded_objects:
self.excluded_objects = sorted(self.excluded_objects + [name])
def _unexclude_object(self, name):
self.gcode.respond_info('Unexcluding object {}'.format(name.upper()))
if name in self.excluded_objects:
excluded_objects = list(self.excluded_objects)
excluded_objects.remove(name)
self.excluded_objects = sorted(excluded_objects)
def _list_objects(self, gcmd):
if gcmd.get('JSON', None) is not None:
object_list = json.dumps(self.objects)
else:
object_list = " ".join(obj['name'] for obj in self.objects)
gcmd.respond_info('Known objects: {}'.format(object_list))
def _list_excluded_objects(self, gcmd):
object_list = " ".join(self.excluded_objects)
gcmd.respond_info('Excluded objects: {}'.format(object_list))
def load_config(config):
return ExcludeObject(config)

Binary file not shown.

View File

@@ -0,0 +1,20 @@
# Code for supporting multiple steppers in single filament extruder.
#
# Copyright (C) 2019 Simo Apell <simo.apell@live.fi>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from kinematics import extruder
class PrinterExtruderStepper:
def __init__(self, config):
self.printer = config.get_printer()
self.extruder_stepper = extruder.ExtruderStepper(config)
self.extruder_name = config.get('extruder')
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
def handle_connect(self):
self.extruder_stepper.sync_to_extruder(self.extruder_name)
def load_config_prefix(config):
return PrinterExtruderStepper(config)

110
klippy/extras/fan.py Normal file
View File

@@ -0,0 +1,110 @@
# Printer cooling fan
#
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import pulse_counter
FAN_MIN_TIME = 0.100
class Fan:
def __init__(self, config, default_shutdown_speed=0.):
self.printer = config.get_printer()
self.last_fan_value = 0.
self.last_fan_time = 0.
# Read config
self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.)
self.kick_start_time = config.getfloat('kick_start_time', 0.1,
minval=0.)
self.off_below = config.getfloat('off_below', default=0.,
minval=0., maxval=1.)
cycle_time = config.getfloat('cycle_time', 0.010, above=0.)
hardware_pwm = config.getboolean('hardware_pwm', False)
shutdown_speed = config.getfloat(
'shutdown_speed', default_shutdown_speed, minval=0., maxval=1.)
# Setup pwm object
ppins = self.printer.lookup_object('pins')
self.mcu_fan = ppins.setup_pin('pwm', config.get('pin'))
self.mcu_fan.setup_max_duration(0.)
self.mcu_fan.setup_cycle_time(cycle_time, hardware_pwm)
shutdown_power = max(0., min(self.max_power, shutdown_speed))
self.mcu_fan.setup_start_value(0., shutdown_power)
# Setup tachometer
self.tachometer = FanTachometer(config)
# Register callbacks
self.printer.register_event_handler("gcode:request_restart",
self._handle_request_restart)
def get_mcu(self):
return self.mcu_fan.get_mcu()
def set_speed(self, print_time, value):
if value < self.off_below:
value = 0.
value = max(0., min(self.max_power, value * self.max_power))
if value == self.last_fan_value:
return
print_time = max(self.last_fan_time + FAN_MIN_TIME, print_time)
if (value and value < self.max_power and self.kick_start_time
and (not self.last_fan_value or value - self.last_fan_value > .5)):
# Run fan at full speed for specified kick_start_time
self.mcu_fan.set_pwm(print_time, self.max_power)
print_time += self.kick_start_time
self.mcu_fan.set_pwm(print_time, value)
self.last_fan_time = print_time
self.last_fan_value = value
def set_speed_from_command(self, value):
toolhead = self.printer.lookup_object('toolhead')
toolhead.register_lookahead_callback((lambda pt:
self.set_speed(pt, value)))
def _handle_request_restart(self, print_time):
self.set_speed(print_time, 0.)
def get_status(self, eventtime):
tachometer_status = self.tachometer.get_status(eventtime)
return {
'speed': self.last_fan_value,
'rpm': tachometer_status['rpm'],
}
class FanTachometer:
def __init__(self, config):
printer = config.get_printer()
self._freq_counter = None
pin = config.get('tachometer_pin', None)
if pin is not None:
self.ppr = config.getint('tachometer_ppr', 2, minval=1)
poll_time = config.getfloat('tachometer_poll_interval',
0.0015, above=0.)
sample_time = 1.
self._freq_counter = pulse_counter.FrequencyCounter(
printer, pin, sample_time, poll_time)
def get_status(self, eventtime):
if self._freq_counter is not None:
rpm = self._freq_counter.get_frequency() * 30. / self.ppr
else:
rpm = None
return {'rpm': rpm}
class PrinterFan:
def __init__(self, config):
self.fan = Fan(config)
# Register commands
gcode = config.get_printer().lookup_object('gcode')
gcode.register_command("M106", self.cmd_M106)
gcode.register_command("M107", self.cmd_M107)
def get_status(self, eventtime):
return self.fan.get_status(eventtime)
def cmd_M106(self, gcmd):
# Set fan speed
value = gcmd.get_float('S', 255., minval=0.) / 255.
self.fan.set_speed_from_command(value)
def cmd_M107(self, gcmd):
# Turn fan off
self.fan.set_speed_from_command(0.)
def load_config(config):
return PrinterFan(config)

BIN
klippy/extras/fan.pyc Normal file

Binary file not shown.

View File

@@ -0,0 +1,28 @@
# Support fans that are controlled by gcode
#
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import fan
class PrinterFanGeneric:
cmd_SET_FAN_SPEED_help = "Sets the speed of a fan"
def __init__(self, config):
self.printer = config.get_printer()
self.fan = fan.Fan(config, default_shutdown_speed=0.)
self.fan_name = config.get_name().split()[-1]
gcode = self.printer.lookup_object("gcode")
gcode.register_mux_command("SET_FAN_SPEED", "FAN",
self.fan_name,
self.cmd_SET_FAN_SPEED,
desc=self.cmd_SET_FAN_SPEED_help)
def get_status(self, eventtime):
return self.fan.get_status(eventtime)
def cmd_SET_FAN_SPEED(self, gcmd):
speed = gcmd.get_float('SPEED', 0.)
self.fan.set_speed_from_command(speed)
def load_config_prefix(config):
return PrinterFanGeneric(config)

Binary file not shown.

View File

@@ -0,0 +1,77 @@
# Filament Motion Sensor Module
#
# Copyright (C) 2021 Joshua Wherrett <thejoshw.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import filament_switch_sensor
CHECK_RUNOUT_TIMEOUT = .250
class EncoderSensor:
def __init__(self, config):
# Read config
self.printer = config.get_printer()
switch_pin = config.get('switch_pin')
self.extruder_name = config.get('extruder')
self.detection_length = config.getfloat(
'detection_length', 7., above=0.)
# Configure pins
buttons = self.printer.load_object(config, 'buttons')
buttons.register_buttons([switch_pin], self.encoder_event)
# Get printer objects
self.reactor = self.printer.get_reactor()
self.runout_helper = filament_switch_sensor.RunoutHelper(config)
self.get_status = self.runout_helper.get_status
self.extruder = None
self.estimated_print_time = None
# Initialise internal state
self.filament_runout_pos = None
# Register commands and event handlers
self.printer.register_event_handler('klippy:ready',
self._handle_ready)
self.printer.register_event_handler('idle_timeout:printing',
self._handle_printing)
self.printer.register_event_handler('idle_timeout:ready',
self._handle_not_printing)
self.printer.register_event_handler('idle_timeout:idle',
self._handle_not_printing)
def _update_filament_runout_pos(self, eventtime=None):
if eventtime is None:
eventtime = self.reactor.monotonic()
self.filament_runout_pos = (
self._get_extruder_pos(eventtime) +
self.detection_length)
def _handle_ready(self):
self.extruder = self.printer.lookup_object(self.extruder_name)
self.estimated_print_time = (
self.printer.lookup_object('mcu').estimated_print_time)
self._update_filament_runout_pos()
self._extruder_pos_update_timer = self.reactor.register_timer(
self._extruder_pos_update_event)
def _handle_printing(self, print_time):
self.reactor.update_timer(self._extruder_pos_update_timer,
self.reactor.NOW)
def _handle_not_printing(self, print_time):
self.reactor.update_timer(self._extruder_pos_update_timer,
self.reactor.NEVER)
def _get_extruder_pos(self, eventtime=None):
if eventtime is None:
eventtime = self.reactor.monotonic()
print_time = self.estimated_print_time(eventtime)
return self.extruder.find_past_position(print_time)
def _extruder_pos_update_event(self, eventtime):
extruder_pos = self._get_extruder_pos(eventtime)
# Check for filament runout
self.runout_helper.note_filament_present(
extruder_pos < self.filament_runout_pos)
return eventtime + CHECK_RUNOUT_TIMEOUT
def encoder_event(self, eventtime, state):
if self.extruder is not None:
self._update_filament_runout_pos(eventtime)
# Check for filament insertion
# Filament is always assumed to be present on an encoder event
self.runout_helper.note_filament_present(True)
def load_config_prefix(config):
return EncoderSensor(config)

View File

@@ -0,0 +1,118 @@
# Generic Filament Sensor Module
#
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
class RunoutHelper:
def __init__(self, config):
self.name = config.get_name().split()[-1]
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.gcode = self.printer.lookup_object('gcode')
# Read config
self.runout_pause = config.getboolean('pause_on_runout', True)
if self.runout_pause:
self.printer.load_object(config, 'pause_resume')
self.runout_gcode = self.insert_gcode = None
gcode_macro = self.printer.load_object(config, 'gcode_macro')
if self.runout_pause or config.get('runout_gcode', None) is not None:
self.runout_gcode = gcode_macro.load_template(
config, 'runout_gcode', '')
if config.get('insert_gcode', None) is not None:
self.insert_gcode = gcode_macro.load_template(
config, 'insert_gcode')
self.pause_delay = config.getfloat('pause_delay', .5, above=.0)
self.event_delay = config.getfloat('event_delay', 3., above=0.)
# Internal state
self.min_event_systime = self.reactor.NEVER
self.filament_present = False
self.sensor_enabled = True
# Register commands and event handlers
self.printer.register_event_handler("klippy:ready", self._handle_ready)
self.gcode.register_mux_command(
"QUERY_FILAMENT_SENSOR", "SENSOR", self.name,
self.cmd_QUERY_FILAMENT_SENSOR,
desc=self.cmd_QUERY_FILAMENT_SENSOR_help)
self.gcode.register_mux_command(
"SET_FILAMENT_SENSOR", "SENSOR", self.name,
self.cmd_SET_FILAMENT_SENSOR,
desc=self.cmd_SET_FILAMENT_SENSOR_help)
def _handle_ready(self):
self.min_event_systime = self.reactor.monotonic() + 2.
def _runout_event_handler(self, eventtime):
# Pausing from inside an event requires that the pause portion
# of pause_resume execute immediately.
pause_prefix = ""
if self.runout_pause:
pause_resume = self.printer.lookup_object('pause_resume')
pause_resume.send_pause_command()
pause_prefix = "PAUSE\n"
self.printer.get_reactor().pause(eventtime + self.pause_delay)
self._exec_gcode(pause_prefix, self.runout_gcode)
def _insert_event_handler(self, eventtime):
self._exec_gcode("", self.insert_gcode)
def _exec_gcode(self, prefix, template):
try:
self.gcode.run_script(prefix + template.render() + "\nM400")
except Exception:
logging.exception("Script running error")
self.min_event_systime = self.reactor.monotonic() + self.event_delay
def note_filament_present(self, is_filament_present):
if is_filament_present == self.filament_present:
return
self.filament_present = is_filament_present
eventtime = self.reactor.monotonic()
if eventtime < self.min_event_systime or not self.sensor_enabled:
# do not process during the initialization time, duplicates,
# during the event delay time, while an event is running, or
# when the sensor is disabled
return
# Determine "printing" status
idle_timeout = self.printer.lookup_object("idle_timeout")
is_printing = idle_timeout.get_status(eventtime)["state"] == "Printing"
# Perform filament action associated with status change (if any)
if is_filament_present:
if not is_printing and self.insert_gcode is not None:
# insert detected
self.min_event_systime = self.reactor.NEVER
logging.info(
"Filament Sensor %s: insert event detected, Time %.2f" %
(self.name, eventtime))
self.reactor.register_callback(self._insert_event_handler)
elif is_printing and self.runout_gcode is not None:
# runout detected
self.min_event_systime = self.reactor.NEVER
logging.info(
"Filament Sensor %s: runout event detected, Time %.2f" %
(self.name, eventtime))
self.reactor.register_callback(self._runout_event_handler)
def get_status(self, eventtime):
return {
"filament_detected": bool(self.filament_present),
"enabled": bool(self.sensor_enabled)}
cmd_QUERY_FILAMENT_SENSOR_help = "Query the status of the Filament Sensor"
def cmd_QUERY_FILAMENT_SENSOR(self, gcmd):
if self.filament_present:
msg = "Filament Sensor %s: filament detected" % (self.name)
else:
msg = "Filament Sensor %s: filament not detected" % (self.name)
gcmd.respond_info(msg)
cmd_SET_FILAMENT_SENSOR_help = "Sets the filament sensor on/off"
def cmd_SET_FILAMENT_SENSOR(self, gcmd):
self.sensor_enabled = gcmd.get_int("ENABLE", 1)
class SwitchSensor:
def __init__(self, config):
printer = config.get_printer()
buttons = printer.load_object(config, 'buttons')
switch_pin = config.get('switch_pin')
buttons.register_buttons([switch_pin], self._button_handler)
self.runout_helper = RunoutHelper(config)
self.get_status = self.runout_helper.get_status
def _button_handler(self, eventtime, state):
self.runout_helper.note_filament_present(state)
def load_config_prefix(config):
return SwitchSensor(config)

Binary file not shown.

View File

@@ -0,0 +1,74 @@
# Support for Marlin/Smoothie/Reprap style firmware retraction via G10/G11
#
# Copyright (C) 2019 Len Trigg <lenbok@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class FirmwareRetraction:
def __init__(self, config):
self.printer = config.get_printer()
self.retract_length = config.getfloat('retract_length', 0., minval=0.)
self.retract_speed = config.getfloat('retract_speed', 20., minval=1)
self.unretract_extra_length = config.getfloat(
'unretract_extra_length', 0., minval=0.)
self.unretract_speed = config.getfloat('unretract_speed', 10., minval=1)
self.unretract_length = (self.retract_length
+ self.unretract_extra_length)
self.is_retracted = False
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command('SET_RETRACTION', self.cmd_SET_RETRACTION,
desc=self.cmd_SET_RETRACTION_help)
self.gcode.register_command('GET_RETRACTION', self.cmd_GET_RETRACTION,
desc=self.cmd_GET_RETRACTION_help)
self.gcode.register_command('G10', self.cmd_G10)
self.gcode.register_command('G11', self.cmd_G11)
def get_status(self, eventtime):
return {
"retract_length": self.retract_length,
"retract_speed": self.retract_speed,
"unretract_extra_length": self.unretract_extra_length,
"unretract_speed": self.unretract_speed,
}
cmd_SET_RETRACTION_help = ("Set firmware retraction parameters")
def cmd_SET_RETRACTION(self, gcmd):
self.retract_length = gcmd.get_float('RETRACT_LENGTH',
self.retract_length, minval=0.)
self.retract_speed = gcmd.get_float('RETRACT_SPEED',
self.retract_speed, minval=1)
self.unretract_extra_length = gcmd.get_float(
'UNRETRACT_EXTRA_LENGTH', self.unretract_extra_length, minval=0.)
self.unretract_speed = gcmd.get_float('UNRETRACT_SPEED',
self.unretract_speed, minval=1)
self.unretract_length = (self.retract_length
+ self.unretract_extra_length)
self.is_retracted = False
cmd_GET_RETRACTION_help = ("Report firmware retraction paramters")
def cmd_GET_RETRACTION(self, gcmd):
gcmd.respond_info("RETRACT_LENGTH=%.5f RETRACT_SPEED=%.5f"
" UNRETRACT_EXTRA_LENGTH=%.5f UNRETRACT_SPEED=%.5f"
% (self.retract_length, self.retract_speed,
self.unretract_extra_length, self.unretract_speed))
def cmd_G10(self, gcmd):
if not self.is_retracted:
self.gcode.run_script_from_command(
"SAVE_GCODE_STATE NAME=_retract_state\n"
"G91\n"
"G1 E-%.5f F%d\n"
"RESTORE_GCODE_STATE NAME=_retract_state"
% (self.retract_length, self.retract_speed*60))
self.is_retracted = True
def cmd_G11(self, gcmd):
if self.is_retracted:
self.gcode.run_script_from_command(
"SAVE_GCODE_STATE NAME=_retract_state\n"
"G91\n"
"G1 E%.5f F%d\n"
"RESTORE_GCODE_STATE NAME=_retract_state"
% (self.unretract_length, self.unretract_speed*60))
self.is_retracted = False
def load_config(config):
return FirmwareRetraction(config)

137
klippy/extras/force_move.py Normal file
View File

@@ -0,0 +1,137 @@
# Utility for manually moving a stepper for diagnostic purposes
#
# Copyright (C) 2018-2019 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging
import chelper
BUZZ_DISTANCE = 1.
BUZZ_VELOCITY = BUZZ_DISTANCE / .250
BUZZ_RADIANS_DISTANCE = math.radians(1.)
BUZZ_RADIANS_VELOCITY = BUZZ_RADIANS_DISTANCE / .250
STALL_TIME = 0.100
# Calculate a move's accel_t, cruise_t, and cruise_v
def calc_move_time(dist, speed, accel):
axis_r = 1.
if dist < 0.:
axis_r = -1.
dist = -dist
if not accel or not dist:
return axis_r, 0., dist / speed, speed
max_cruise_v2 = dist * accel
if max_cruise_v2 < speed**2:
speed = math.sqrt(max_cruise_v2)
accel_t = speed / accel
accel_decel_d = accel_t * speed
cruise_t = (dist - accel_decel_d) / speed
return axis_r, accel_t, cruise_t, speed
class ForceMove:
def __init__(self, config):
self.printer = config.get_printer()
self.steppers = {}
# Setup iterative solver
ffi_main, ffi_lib = chelper.get_ffi()
self.trapq = ffi_main.gc(ffi_lib.trapq_alloc(), ffi_lib.trapq_free)
self.trapq_append = ffi_lib.trapq_append
self.trapq_finalize_moves = ffi_lib.trapq_finalize_moves
self.stepper_kinematics = ffi_main.gc(
ffi_lib.cartesian_stepper_alloc(b'x'), ffi_lib.free)
# Register commands
gcode = self.printer.lookup_object('gcode')
gcode.register_command('STEPPER_BUZZ', self.cmd_STEPPER_BUZZ,
desc=self.cmd_STEPPER_BUZZ_help)
gcode.register_command('SET_KINEMATIC_POSITION',
self.cmd_SET_KINEMATIC_POSITION,
desc=self.cmd_SET_KINEMATIC_POSITION_help)
if config.getboolean("enable_force_move", False):
gcode.register_command('FORCE_MOVE', self.cmd_FORCE_MOVE,
desc=self.cmd_FORCE_MOVE_help)
def register_stepper(self, config, mcu_stepper):
self.steppers[mcu_stepper.get_name()] = mcu_stepper
def lookup_stepper(self, name):
if name not in self.steppers:
raise self.printer.config_error("Unknown stepper %s" % (name,))
return self.steppers[name]
def _force_enable(self, stepper):
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
stepper_enable = self.printer.lookup_object('stepper_enable')
enable = stepper_enable.lookup_enable(stepper.get_name())
was_enable = enable.is_motor_enabled()
if not was_enable:
enable.motor_enable(print_time)
toolhead.dwell(STALL_TIME)
return was_enable
def _restore_enable(self, stepper, was_enable):
if not was_enable:
toolhead = self.printer.lookup_object('toolhead')
toolhead.dwell(STALL_TIME)
print_time = toolhead.get_last_move_time()
stepper_enable = self.printer.lookup_object('stepper_enable')
enable = stepper_enable.lookup_enable(stepper.get_name())
enable.motor_disable(print_time)
toolhead.dwell(STALL_TIME)
def manual_move(self, stepper, dist, speed, accel=0.):
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
prev_sk = stepper.set_stepper_kinematics(self.stepper_kinematics)
prev_trapq = stepper.set_trapq(self.trapq)
stepper.set_position((0., 0., 0.))
axis_r, accel_t, cruise_t, cruise_v = calc_move_time(dist, speed, accel)
print_time = toolhead.get_last_move_time()
self.trapq_append(self.trapq, print_time, accel_t, cruise_t, accel_t,
0., 0., 0., axis_r, 0., 0., 0., cruise_v, accel)
print_time = print_time + accel_t + cruise_t + accel_t
stepper.generate_steps(print_time)
self.trapq_finalize_moves(self.trapq, print_time + 99999.9)
stepper.set_trapq(prev_trapq)
stepper.set_stepper_kinematics(prev_sk)
toolhead.note_kinematic_activity(print_time)
toolhead.dwell(accel_t + cruise_t + accel_t)
def _lookup_stepper(self, gcmd):
name = gcmd.get('STEPPER')
if name not in self.steppers:
raise gcmd.error("Unknown stepper %s" % (name,))
return self.steppers[name]
cmd_STEPPER_BUZZ_help = "Oscillate a given stepper to help id it"
def cmd_STEPPER_BUZZ(self, gcmd):
stepper = self._lookup_stepper(gcmd)
logging.info("Stepper buzz %s", stepper.get_name())
was_enable = self._force_enable(stepper)
toolhead = self.printer.lookup_object('toolhead')
dist, speed = BUZZ_DISTANCE, BUZZ_VELOCITY
if stepper.units_in_radians():
dist, speed = BUZZ_RADIANS_DISTANCE, BUZZ_RADIANS_VELOCITY
for i in range(10):
self.manual_move(stepper, dist, speed)
toolhead.dwell(.050)
self.manual_move(stepper, -dist, speed)
toolhead.dwell(.450)
self._restore_enable(stepper, was_enable)
cmd_FORCE_MOVE_help = "Manually move a stepper; invalidates kinematics"
def cmd_FORCE_MOVE(self, gcmd):
stepper = self._lookup_stepper(gcmd)
distance = gcmd.get_float('DISTANCE')
speed = gcmd.get_float('VELOCITY', above=0.)
accel = gcmd.get_float('ACCEL', 0., minval=0.)
logging.info("FORCE_MOVE %s distance=%.3f velocity=%.3f accel=%.3f",
stepper.get_name(), distance, speed, accel)
self._force_enable(stepper)
self.manual_move(stepper, distance, speed, accel)
cmd_SET_KINEMATIC_POSITION_help = "Force a low-level kinematic position"
def cmd_SET_KINEMATIC_POSITION(self, gcmd):
toolhead = self.printer.lookup_object('toolhead')
toolhead.get_last_move_time()
curpos = toolhead.get_position()
x = gcmd.get_float('X', curpos[0])
y = gcmd.get_float('Y', curpos[1])
z = gcmd.get_float('Z', curpos[2])
logging.info("SET_KINEMATIC_POSITION pos=%.3f,%.3f,%.3f", x, y, z)
toolhead.set_position([x, y, z, curpos[3]], homing_axes=(0, 1, 2))
def load_config(config):
return ForceMove(config)

Binary file not shown.

134
klippy/extras/gcode_arcs.py Normal file
View File

@@ -0,0 +1,134 @@
# adds support fro ARC commands via G2/G3
#
# Copyright (C) 2019 Aleksej Vasiljkovic <achmed21@gmail.com>
#
# function planArc() originates from https://github.com/MarlinFirmware/Marlin
# Copyright (C) 2011 Camiel Gubbels / Erik van der Zalm
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math
# Coordinates created by this are converted into G1 commands.
#
# note: only IJ version available
class ArcSupport:
def __init__(self, config):
self.printer = config.get_printer()
self.mm_per_arc_segment = config.getfloat('resolution', 1., above=0.0)
self.gcode_move = self.printer.load_object(config, 'gcode_move')
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command("G2", self.cmd_G2)
self.gcode.register_command("G3", self.cmd_G3)
def cmd_G2(self, gcmd):
self._cmd_inner(gcmd, True)
def cmd_G3(self, gcmd):
self._cmd_inner(gcmd, False)
def _cmd_inner(self, gcmd, clockwise):
gcodestatus = self.gcode_move.get_status()
if not gcodestatus['absolute_coordinates']:
raise gcmd.error("G2/G3 does not support relative move mode")
currentPos = gcodestatus['gcode_position']
# Parse parameters
asX = gcmd.get_float("X", currentPos[0])
asY = gcmd.get_float("Y", currentPos[1])
asZ = gcmd.get_float("Z", currentPos[2])
if gcmd.get_float("R", None) is not None:
raise gcmd.error("G2/G3 does not support R moves")
asI = gcmd.get_float("I", 0.)
asJ = gcmd.get_float("J", 0.)
if not asI and not asJ:
raise gcmd.error("G2/G3 neither I nor J given")
asE = gcmd.get_float("E", None)
asF = gcmd.get_float("F", None)
# Build list of linear coordinates to move to
coords = self.planArc(currentPos, [asX, asY, asZ], [asI, asJ],
clockwise)
e_per_move = e_base = 0.
if asE is not None:
if gcodestatus['absolute_extrude']:
e_base = currentPos[3]
e_per_move = (asE - e_base) / len(coords)
# Convert coords into G1 commands
for coord in coords:
g1_params = {'X': coord[0], 'Y': coord[1], 'Z': coord[2]}
if e_per_move:
g1_params['E'] = e_base + e_per_move
if gcodestatus['absolute_extrude']:
e_base += e_per_move
if asF is not None:
g1_params['F'] = asF
g1_gcmd = self.gcode.create_gcode_command("G1", "G1", g1_params)
self.gcode_move.cmd_G1(g1_gcmd)
# function planArc() originates from marlin plan_arc()
# https://github.com/MarlinFirmware/Marlin
#
# The arc is approximated by generating many small linear segments.
# The length of each segment is configured in MM_PER_ARC_SEGMENT
# Arcs smaller then this value, will be a Line only
def planArc(self, currentPos, targetPos, offset, clockwise):
# todo: sometimes produces full circles
X_AXIS = 0
Y_AXIS = 1
Z_AXIS = 2
# Radius vector from center to current location
r_P = -offset[0]
r_Q = -offset[1]
# Determine angular travel
center_P = currentPos[X_AXIS] - r_P
center_Q = currentPos[Y_AXIS] - r_Q
rt_X = targetPos[X_AXIS] - center_P
rt_Y = targetPos[Y_AXIS] - center_Q
angular_travel = math.atan2(r_P * rt_Y - r_Q * rt_X,
r_P * rt_X + r_Q * rt_Y)
if angular_travel < 0.:
angular_travel += 2. * math.pi
if clockwise:
angular_travel -= 2. * math.pi
if (angular_travel == 0.
and currentPos[X_AXIS] == targetPos[X_AXIS]
and currentPos[Y_AXIS] == targetPos[Y_AXIS]):
# Make a circle if the angular rotation is 0 and the
# target is current position
angular_travel = 2. * math.pi
# Determine number of segments
linear_travel = targetPos[Z_AXIS] - currentPos[Z_AXIS]
radius = math.hypot(r_P, r_Q)
flat_mm = radius * angular_travel
if linear_travel:
mm_of_travel = math.hypot(flat_mm, linear_travel)
else:
mm_of_travel = math.fabs(flat_mm)
segments = max(1., math.floor(mm_of_travel / self.mm_per_arc_segment))
# Generate coordinates
theta_per_segment = angular_travel / segments
linear_per_segment = linear_travel / segments
coords = []
for i in range(1, int(segments)):
dist_Z = i * linear_per_segment
cos_Ti = math.cos(i * theta_per_segment)
sin_Ti = math.sin(i * theta_per_segment)
r_P = -offset[0] * cos_Ti + offset[1] * sin_Ti
r_Q = -offset[0] * sin_Ti - offset[1] * cos_Ti
c = [center_P + r_P, center_Q + r_Q, currentPos[Z_AXIS] + dist_Z]
coords.append(c)
coords.append(targetPos)
return coords
def load_config(config):
return ArcSupport(config)

Binary file not shown.

View File

@@ -0,0 +1,51 @@
# Support for executing gcode when a hardware button is pressed or released.
#
# Copyright (C) 2019 Alec Plumb <alec@etherwalker.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
class GCodeButton:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split(' ')[-1]
self.pin = config.get('pin')
self.last_state = 0
buttons = self.printer.load_object(config, "buttons")
if config.get('analog_range', None) is None:
buttons.register_buttons([self.pin], self.button_callback)
else:
amin, amax = config.getfloatlist('analog_range', count=2)
pullup = config.getfloat('analog_pullup_resistor', 4700., above=0.)
buttons.register_adc_button(self.pin, amin, amax, pullup,
self.button_callback)
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.press_template = gcode_macro.load_template(config, 'press_gcode')
self.release_template = gcode_macro.load_template(config,
'release_gcode', '')
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_mux_command("QUERY_BUTTON", "BUTTON", self.name,
self.cmd_QUERY_BUTTON,
desc=self.cmd_QUERY_BUTTON_help)
cmd_QUERY_BUTTON_help = "Report on the state of a button"
def cmd_QUERY_BUTTON(self, gcmd):
gcmd.respond_info(self.name + ": " + self.get_status()['state'])
def button_callback(self, eventtime, state):
self.last_state = state
template = self.press_template
if not state:
template = self.release_template
try:
self.gcode.run_script(template.render())
except:
logging.exception("Script running error")
def get_status(self, eventtime=None):
if self.last_state:
return {'state': "PRESSED"}
return {'state': "RELEASED"}
def load_config_prefix(config):
return GCodeButton(config)

View File

@@ -0,0 +1,191 @@
# Add ability to define custom g-code macros
#
# Copyright (C) 2018-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import traceback, logging, ast, copy
import jinja2
######################################################################
# Template handling
######################################################################
# Wrapper for access to printer object get_status() methods
class GetStatusWrapper:
def __init__(self, printer, eventtime=None):
self.printer = printer
self.eventtime = eventtime
self.cache = {}
def __getitem__(self, val):
sval = str(val).strip()
if sval in self.cache:
return self.cache[sval]
po = self.printer.lookup_object(sval, None)
if po is None or not hasattr(po, 'get_status'):
raise KeyError(val)
if self.eventtime is None:
self.eventtime = self.printer.get_reactor().monotonic()
self.cache[sval] = res = copy.deepcopy(po.get_status(self.eventtime))
return res
def __contains__(self, val):
try:
self.__getitem__(val)
except KeyError as e:
return False
return True
def __iter__(self):
for name, obj in self.printer.lookup_objects():
if self.__contains__(name):
yield name
# Wrapper around a Jinja2 template
class TemplateWrapper:
def __init__(self, printer, env, name, script):
self.printer = printer
self.name = name
self.gcode = self.printer.lookup_object('gcode')
gcode_macro = self.printer.lookup_object('gcode_macro')
self.create_template_context = gcode_macro.create_template_context
try:
self.template = env.from_string(script)
except Exception as e:
msg = "Error loading template '%s': %s" % (
name, traceback.format_exception_only(type(e), e)[-1])
logging.exception(msg)
raise printer.config_error(msg)
def render(self, context=None):
if context is None:
context = self.create_template_context()
try:
return str(self.template.render(context))
except Exception as e:
msg = "Error evaluating '%s': %s" % (
self.name, traceback.format_exception_only(type(e), e)[-1])
logging.exception(msg)
raise self.gcode.error(msg)
def run_gcode_from_command(self, context=None):
self.gcode.run_script_from_command(self.render(context))
# Main gcode macro template tracking
class PrinterGCodeMacro:
def __init__(self, config):
self.printer = config.get_printer()
self.env = jinja2.Environment('{%', '%}', '{', '}')
def load_template(self, config, option, default=None):
name = "%s:%s" % (config.get_name(), option)
if default is None:
script = config.get(option)
else:
script = config.get(option, default)
return TemplateWrapper(self.printer, self.env, name, script)
def _action_emergency_stop(self, msg="action_emergency_stop"):
self.printer.invoke_shutdown("Shutdown due to %s" % (msg,))
return ""
def _action_respond_info(self, msg):
self.printer.lookup_object('gcode').respond_info(msg)
return ""
def _action_raise_error(self, msg):
raise self.printer.command_error(msg)
def _action_call_remote_method(self, method, **kwargs):
webhooks = self.printer.lookup_object('webhooks')
try:
webhooks.call_remote_method(method, **kwargs)
except self.printer.command_error:
logging.exception("Remote Call Error")
return ""
def create_template_context(self, eventtime=None):
return {
'printer': GetStatusWrapper(self.printer, eventtime),
'action_emergency_stop': self._action_emergency_stop,
'action_respond_info': self._action_respond_info,
'action_raise_error': self._action_raise_error,
'action_call_remote_method': self._action_call_remote_method,
}
def load_config(config):
return PrinterGCodeMacro(config)
######################################################################
# GCode macro
######################################################################
class GCodeMacro:
def __init__(self, config):
if len(config.get_name().split()) > 2:
raise config.error(
"Name of section '%s' contains illegal whitespace"
% (config.get_name()))
name = config.get_name().split()[1]
self.alias = name.upper()
self.printer = printer = config.get_printer()
gcode_macro = printer.load_object(config, 'gcode_macro')
self.template = gcode_macro.load_template(config, 'gcode')
self.gcode = printer.lookup_object('gcode')
self.rename_existing = config.get("rename_existing", None)
self.cmd_desc = config.get("description", "G-Code macro")
if self.rename_existing is not None:
if (self.gcode.is_traditional_gcode(self.alias)
!= self.gcode.is_traditional_gcode(self.rename_existing)):
raise config.error(
"G-Code macro rename of different types ('%s' vs '%s')"
% (self.alias, self.rename_existing))
printer.register_event_handler("klippy:connect",
self.handle_connect)
else:
self.gcode.register_command(self.alias, self.cmd,
desc=self.cmd_desc)
self.gcode.register_mux_command("SET_GCODE_VARIABLE", "MACRO",
name, self.cmd_SET_GCODE_VARIABLE,
desc=self.cmd_SET_GCODE_VARIABLE_help)
self.in_script = False
self.variables = {}
prefix = 'variable_'
for option in config.get_prefix_options(prefix):
try:
self.variables[option[len(prefix):]] = ast.literal_eval(
config.get(option))
except ValueError as e:
raise config.error(
"Option '%s' in section '%s' is not a valid literal" % (
option, config.get_name()))
def handle_connect(self):
prev_cmd = self.gcode.register_command(self.alias, None)
if prev_cmd is None:
raise self.printer.config_error(
"Existing command '%s' not found in gcode_macro rename"
% (self.alias,))
pdesc = "Renamed builtin of '%s'" % (self.alias,)
self.gcode.register_command(self.rename_existing, prev_cmd, desc=pdesc)
self.gcode.register_command(self.alias, self.cmd, desc=self.cmd_desc)
def get_status(self, eventtime):
return self.variables
cmd_SET_GCODE_VARIABLE_help = "Set the value of a G-Code macro variable"
def cmd_SET_GCODE_VARIABLE(self, gcmd):
variable = gcmd.get('VARIABLE')
value = gcmd.get('VALUE')
if variable not in self.variables:
raise gcmd.error("Unknown gcode_macro variable '%s'" % (variable,))
try:
literal = ast.literal_eval(value)
except ValueError as e:
raise gcmd.error("Unable to parse '%s' as a literal" % (value,))
v = dict(self.variables)
v[variable] = literal
self.variables = v
def cmd(self, gcmd):
if self.in_script:
raise gcmd.error("Macro %s called recursively" % (self.alias,))
kwparams = dict(self.variables)
kwparams.update(self.template.create_template_context())
kwparams['params'] = gcmd.get_command_parameters()
kwparams['rawparams'] = gcmd.get_raw_command_parameters()
self.in_script = True
try:
self.template.run_gcode_from_command(kwparams)
finally:
self.in_script = False
def load_config_prefix(config):
return GCodeMacro(config)

Binary file not shown.

276
klippy/extras/gcode_move.py Normal file
View File

@@ -0,0 +1,276 @@
# G-Code G1 movement commands (and associated coordinate manipulation)
#
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
class GCodeMove:
def __init__(self, config):
self.printer = printer = config.get_printer()
printer.register_event_handler("klippy:ready", self._handle_ready)
printer.register_event_handler("klippy:shutdown", self._handle_shutdown)
printer.register_event_handler("toolhead:set_position",
self.reset_last_position)
printer.register_event_handler("toolhead:manual_move",
self.reset_last_position)
printer.register_event_handler("gcode:command_error",
self.reset_last_position)
printer.register_event_handler("extruder:activate_extruder",
self._handle_activate_extruder)
printer.register_event_handler("homing:home_rails_end",
self._handle_home_rails_end)
self.is_printer_ready = False
# Register g-code commands
gcode = printer.lookup_object('gcode')
handlers = [
'G1', 'G20', 'G21',
'M82', 'M83', 'G90', 'G91', 'G92', 'M220', 'M221',
'SET_GCODE_OFFSET', 'SAVE_GCODE_STATE', 'RESTORE_GCODE_STATE',
]
for cmd in handlers:
func = getattr(self, 'cmd_' + cmd)
desc = getattr(self, 'cmd_' + cmd + '_help', None)
gcode.register_command(cmd, func, False, desc)
gcode.register_command('G0', self.cmd_G1)
gcode.register_command('M114', self.cmd_M114, True)
gcode.register_command('GET_POSITION', self.cmd_GET_POSITION, True,
desc=self.cmd_GET_POSITION_help)
self.Coord = gcode.Coord
# G-Code coordinate manipulation
self.absolute_coord = self.absolute_extrude = True
self.base_position = [0.0, 0.0, 0.0, 0.0]
self.last_position = [0.0, 0.0, 0.0, 0.0]
self.homing_position = [0.0, 0.0, 0.0, 0.0]
self.speed = 25.
self.speed_factor = 1. / 60.
self.extrude_factor = 1.
# G-Code state
self.saved_states = {}
self.move_transform = self.move_with_transform = None
self.position_with_transform = (lambda: [0., 0., 0., 0.])
def _handle_ready(self):
self.is_printer_ready = True
if self.move_transform is None:
toolhead = self.printer.lookup_object('toolhead')
self.move_with_transform = toolhead.move
self.position_with_transform = toolhead.get_position
self.reset_last_position()
def _handle_shutdown(self):
if not self.is_printer_ready:
return
self.is_printer_ready = False
logging.info("gcode state: absolute_coord=%s absolute_extrude=%s"
" base_position=%s last_position=%s homing_position=%s"
" speed_factor=%s extrude_factor=%s speed=%s",
self.absolute_coord, self.absolute_extrude,
self.base_position, self.last_position,
self.homing_position, self.speed_factor,
self.extrude_factor, self.speed)
def _handle_activate_extruder(self):
self.reset_last_position()
self.extrude_factor = 1.
self.base_position[3] = self.last_position[3]
def _handle_home_rails_end(self, homing_state, rails):
self.reset_last_position()
for axis in homing_state.get_axes():
self.base_position[axis] = self.homing_position[axis]
def set_move_transform(self, transform, force=False):
if self.move_transform is not None and not force:
raise self.printer.config_error(
"G-Code move transform already specified")
old_transform = self.move_transform
if old_transform is None:
old_transform = self.printer.lookup_object('toolhead', None)
self.move_transform = transform
self.move_with_transform = transform.move
self.position_with_transform = transform.get_position
return old_transform
def _get_gcode_position(self):
p = [lp - bp for lp, bp in zip(self.last_position, self.base_position)]
p[3] /= self.extrude_factor
return p
def _get_gcode_speed(self):
return self.speed / self.speed_factor
def _get_gcode_speed_override(self):
return self.speed_factor * 60.
def get_status(self, eventtime=None):
move_position = self._get_gcode_position()
return {
'speed_factor': self._get_gcode_speed_override(),
'speed': self._get_gcode_speed(),
'extrude_factor': self.extrude_factor,
'absolute_coordinates': self.absolute_coord,
'absolute_extrude': self.absolute_extrude,
'homing_origin': self.Coord(*self.homing_position),
'position': self.Coord(*self.last_position),
'gcode_position': self.Coord(*move_position),
}
def reset_last_position(self):
if self.is_printer_ready:
self.last_position = self.position_with_transform()
# G-Code movement commands
def cmd_G1(self, gcmd):
# Move
params = gcmd.get_command_parameters()
try:
for pos, axis in enumerate('XYZ'):
if axis in params:
v = float(params[axis])
if not self.absolute_coord:
# value relative to position of last move
self.last_position[pos] += v
else:
# value relative to base coordinate position
self.last_position[pos] = v + self.base_position[pos]
if 'E' in params:
v = float(params['E']) * self.extrude_factor
if not self.absolute_coord or not self.absolute_extrude:
# value relative to position of last move
self.last_position[3] += v
else:
# value relative to base coordinate position
self.last_position[3] = v + self.base_position[3]
if 'F' in params:
gcode_speed = float(params['F'])
if gcode_speed <= 0.:
raise gcmd.error("Invalid speed in '%s'"
% (gcmd.get_commandline(),))
self.speed = gcode_speed * self.speed_factor
except ValueError as e:
raise gcmd.error("Unable to parse move '%s'"
% (gcmd.get_commandline(),))
self.move_with_transform(self.last_position, self.speed)
# G-Code coordinate manipulation
def cmd_G20(self, gcmd):
# Set units to inches
raise gcmd.error('Machine does not support G20 (inches) command')
def cmd_G21(self, gcmd):
# Set units to millimeters
pass
def cmd_M82(self, gcmd):
# Use absolute distances for extrusion
self.absolute_extrude = True
def cmd_M83(self, gcmd):
# Use relative distances for extrusion
self.absolute_extrude = False
def cmd_G90(self, gcmd):
# Use absolute coordinates
self.absolute_coord = True
def cmd_G91(self, gcmd):
# Use relative coordinates
self.absolute_coord = False
def cmd_G92(self, gcmd):
# Set position
offsets = [ gcmd.get_float(a, None) for a in 'XYZE' ]
for i, offset in enumerate(offsets):
if offset is not None:
if i == 3:
offset *= self.extrude_factor
self.base_position[i] = self.last_position[i] - offset
if offsets == [None, None, None, None]:
self.base_position = list(self.last_position)
def cmd_M114(self, gcmd):
# Get Current Position
p = self._get_gcode_position()
gcmd.respond_raw("X:%.3f Y:%.3f Z:%.3f E:%.3f" % tuple(p))
def cmd_M220(self, gcmd):
# Set speed factor override percentage
value = gcmd.get_float('S', 100., above=0.) / (60. * 100.)
self.speed = self._get_gcode_speed() * value
self.speed_factor = value
def cmd_M221(self, gcmd):
# Set extrude factor override percentage
new_extrude_factor = gcmd.get_float('S', 100., above=0.) / 100.
last_e_pos = self.last_position[3]
e_value = (last_e_pos - self.base_position[3]) / self.extrude_factor
self.base_position[3] = last_e_pos - e_value * new_extrude_factor
self.extrude_factor = new_extrude_factor
cmd_SET_GCODE_OFFSET_help = "Set a virtual offset to g-code positions"
def cmd_SET_GCODE_OFFSET(self, gcmd):
move_delta = [0., 0., 0., 0.]
for pos, axis in enumerate('XYZE'):
offset = gcmd.get_float(axis, None)
if offset is None:
offset = gcmd.get_float(axis + '_ADJUST', None)
if offset is None:
continue
offset += self.homing_position[pos]
delta = offset - self.homing_position[pos]
move_delta[pos] = delta
self.base_position[pos] += delta
self.homing_position[pos] = offset
# Move the toolhead the given offset if requested
if gcmd.get_int('MOVE', 0):
speed = gcmd.get_float('MOVE_SPEED', self.speed, above=0.)
for pos, delta in enumerate(move_delta):
self.last_position[pos] += delta
self.move_with_transform(self.last_position, speed)
cmd_SAVE_GCODE_STATE_help = "Save G-Code coordinate state"
def cmd_SAVE_GCODE_STATE(self, gcmd):
state_name = gcmd.get('NAME', 'default')
self.saved_states[state_name] = {
'absolute_coord': self.absolute_coord,
'absolute_extrude': self.absolute_extrude,
'base_position': list(self.base_position),
'last_position': list(self.last_position),
'homing_position': list(self.homing_position),
'speed': self.speed, 'speed_factor': self.speed_factor,
'extrude_factor': self.extrude_factor,
}
cmd_RESTORE_GCODE_STATE_help = "Restore a previously saved G-Code state"
def cmd_RESTORE_GCODE_STATE(self, gcmd):
state_name = gcmd.get('NAME', 'default')
state = self.saved_states.get(state_name)
if state is None:
raise gcmd.error("Unknown g-code state: %s" % (state_name,))
# Restore state
self.absolute_coord = state['absolute_coord']
self.absolute_extrude = state['absolute_extrude']
self.base_position = list(state['base_position'])
self.homing_position = list(state['homing_position'])
self.speed = state['speed']
self.speed_factor = state['speed_factor']
self.extrude_factor = state['extrude_factor']
# Restore the relative E position
e_diff = self.last_position[3] - state['last_position'][3]
self.base_position[3] += e_diff
# Move the toolhead back if requested
if gcmd.get_int('MOVE', 0):
speed = gcmd.get_float('MOVE_SPEED', self.speed, above=0.)
self.last_position[:3] = state['last_position'][:3]
self.move_with_transform(self.last_position, speed)
cmd_GET_POSITION_help = (
"Return information on the current location of the toolhead")
def cmd_GET_POSITION(self, gcmd):
toolhead = self.printer.lookup_object('toolhead', None)
if toolhead is None:
raise gcmd.error("Printer not ready")
kin = toolhead.get_kinematics()
steppers = kin.get_steppers()
mcu_pos = " ".join(["%s:%d" % (s.get_name(), s.get_mcu_position())
for s in steppers])
cinfo = [(s.get_name(), s.get_commanded_position()) for s in steppers]
stepper_pos = " ".join(["%s:%.6f" % (a, v) for a, v in cinfo])
kinfo = zip("XYZ", kin.calc_position(dict(cinfo)))
kin_pos = " ".join(["%s:%.6f" % (a, v) for a, v in kinfo])
toolhead_pos = " ".join(["%s:%.6f" % (a, v) for a, v in zip(
"XYZE", toolhead.get_position())])
gcode_pos = " ".join(["%s:%.6f" % (a, v)
for a, v in zip("XYZE", self.last_position)])
base_pos = " ".join(["%s:%.6f" % (a, v)
for a, v in zip("XYZE", self.base_position)])
homing_pos = " ".join(["%s:%.6f" % (a, v)
for a, v in zip("XYZ", self.homing_position)])
gcmd.respond_info("mcu: %s\n"
"stepper: %s\n"
"kinematic: %s\n"
"toolhead: %s\n"
"gcode: %s\n"
"gcode base: %s\n"
"gcode homing: %s"
% (mcu_pos, stepper_pos, kin_pos, toolhead_pos,
gcode_pos, base_pos, homing_pos))
def load_config(config):
return GCodeMove(config)

Binary file not shown.

View File

@@ -0,0 +1,224 @@
# Support for filament width sensor
#
# Copyright (C) 2019 Mustafa YILDIZ <mydiz@hotmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import filament_switch_sensor
ADC_REPORT_TIME = 0.500
ADC_SAMPLE_TIME = 0.03
ADC_SAMPLE_COUNT = 15
class HallFilamentWidthSensor:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.pin1 = config.get('adc1')
self.pin2 = config.get('adc2')
self.dia1=config.getfloat('Cal_dia1', 1.5)
self.dia2=config.getfloat('Cal_dia2', 2.0)
self.rawdia1=config.getint('Raw_dia1', 9500)
self.rawdia2=config.getint('Raw_dia2', 10500)
self.MEASUREMENT_INTERVAL_MM=config.getint('measurement_interval',10)
self.nominal_filament_dia = config.getfloat(
'default_nominal_filament_diameter', above=1)
self.measurement_delay = config.getfloat('measurement_delay', above=0.)
self.measurement_max_difference = config.getfloat('max_difference', 0.2)
self.max_diameter = (self.nominal_filament_dia
+ self.measurement_max_difference)
self.min_diameter = (self.nominal_filament_dia
- self.measurement_max_difference)
self.diameter =self.nominal_filament_dia
self.is_active =config.getboolean('enable', False)
self.runout_dia=config.getfloat('min_diameter', 1.0)
self.is_log =config.getboolean('logging', False)
# Use the current diameter instead of nominal while the first
# measurement isn't in place
self.use_current_dia_while_delay = config.getboolean(
'use_current_dia_while_delay', False)
# filament array [position, filamentWidth]
self.filament_array = []
self.lastFilamentWidthReading = 0
self.lastFilamentWidthReading2 = 0
self.firstExtruderUpdatePosition = 0
self.filament_width = self.nominal_filament_dia
# printer objects
self.toolhead = self.ppins = self.mcu_adc = None
self.printer.register_event_handler("klippy:ready", self.handle_ready)
# Start adc
self.ppins = self.printer.lookup_object('pins')
self.mcu_adc = self.ppins.setup_pin('adc', self.pin1)
self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback)
self.mcu_adc2 = self.ppins.setup_pin('adc', self.pin2)
self.mcu_adc2.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc2.setup_adc_callback(ADC_REPORT_TIME, self.adc2_callback)
# extrude factor updating
self.extrude_factor_update_timer = self.reactor.register_timer(
self.extrude_factor_update_event)
# Register commands
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command('QUERY_FILAMENT_WIDTH', self.cmd_M407)
self.gcode.register_command('RESET_FILAMENT_WIDTH_SENSOR',
self.cmd_ClearFilamentArray)
self.gcode.register_command('DISABLE_FILAMENT_WIDTH_SENSOR',
self.cmd_M406)
self.gcode.register_command('ENABLE_FILAMENT_WIDTH_SENSOR',
self.cmd_M405)
self.gcode.register_command('QUERY_RAW_FILAMENT_WIDTH',
self.cmd_Get_Raw_Values)
self.gcode.register_command('ENABLE_FILAMENT_WIDTH_LOG',
self.cmd_log_enable)
self.gcode.register_command('DISABLE_FILAMENT_WIDTH_LOG',
self.cmd_log_disable)
self.runout_helper = filament_switch_sensor.RunoutHelper(config)
# Initialization
def handle_ready(self):
# Load printer objects
self.toolhead = self.printer.lookup_object('toolhead')
# Start extrude factor update timer
self.reactor.update_timer(self.extrude_factor_update_timer,
self.reactor.NOW)
def adc_callback(self, read_time, read_value):
# read sensor value
self.lastFilamentWidthReading = round(read_value * 10000)
def adc2_callback(self, read_time, read_value):
# read sensor value
self.lastFilamentWidthReading2 = round(read_value * 10000)
# calculate diameter
diameter_new = round((self.dia2 - self.dia1)/
(self.rawdia2-self.rawdia1)*
((self.lastFilamentWidthReading+self.lastFilamentWidthReading2)
-self.rawdia1)+self.dia1,2)
self.diameter=(5.0 * self.diameter + diameter_new)/6
def update_filament_array(self, last_epos):
# Fill array
if len(self.filament_array) > 0:
# Get last reading position in array & calculate next
# reading position
next_reading_position = (self.filament_array[-1][0] +
self.MEASUREMENT_INTERVAL_MM)
if next_reading_position <= (last_epos + self.measurement_delay):
self.filament_array.append([last_epos + self.measurement_delay,
self.diameter])
if self.is_log:
self.gcode.respond_info("Filament width:%.3f" %
( self.diameter ))
else:
# add first item to array
self.filament_array.append([self.measurement_delay + last_epos,
self.diameter])
self.firstExtruderUpdatePosition = (self.measurement_delay
+ last_epos)
def extrude_factor_update_event(self, eventtime):
# Update extrude factor
pos = self.toolhead.get_position()
last_epos = pos[3]
# Update filament array for lastFilamentWidthReading
self.update_filament_array(last_epos)
# Check runout
self.runout_helper.note_filament_present(
self.diameter > self.runout_dia)
# Does filament exists
if self.diameter > 0.5:
if len(self.filament_array) > 0:
# Get first position in filament array
pending_position = self.filament_array[0][0]
if pending_position <= last_epos:
# Get first item in filament_array queue
item = self.filament_array.pop(0)
self.filament_width = item[1]
else:
if ((self.use_current_dia_while_delay)
and (self.firstExtruderUpdatePosition
== pending_position)):
self.filament_width = self.diameter
elif self.firstExtruderUpdatePosition == pending_position:
self.filament_width = self.nominal_filament_dia
if ((self.filament_width <= self.max_diameter)
and (self.filament_width >= self.min_diameter)):
percentage = round(self.nominal_filament_dia**2
/ self.filament_width**2 * 100)
self.gcode.run_script("M221 S" + str(percentage))
else:
self.gcode.run_script("M221 S100")
else:
self.gcode.run_script("M221 S100")
self.filament_array = []
if self.is_active:
return eventtime + 1
else:
return self.reactor.NEVER
def cmd_M407(self, gcmd):
response = ""
if self.diameter > 0:
response += ("Filament dia (measured mm): "
+ str(self.diameter))
else:
response += "Filament NOT present"
gcmd.respond_info(response)
def cmd_ClearFilamentArray(self, gcmd):
self.filament_array = []
gcmd.respond_info("Filament width measurements cleared!")
# Set extrude multiplier to 100%
self.gcode.run_script_from_command("M221 S100")
def cmd_M405(self, gcmd):
response = "Filament width sensor Turned On"
if self.is_active:
response = "Filament width sensor is already On"
else:
self.is_active = True
# Start extrude factor update timer
self.reactor.update_timer(self.extrude_factor_update_timer,
self.reactor.NOW)
gcmd.respond_info(response)
def cmd_M406(self, gcmd):
response = "Filament width sensor Turned Off"
if not self.is_active:
response = "Filament width sensor is already Off"
else:
self.is_active = False
# Stop extrude factor update timer
self.reactor.update_timer(self.extrude_factor_update_timer,
self.reactor.NEVER)
# Clear filament array
self.filament_array = []
# Set extrude multiplier to 100%
self.gcode.run_script_from_command("M221 S100")
gcmd.respond_info(response)
def cmd_Get_Raw_Values(self, gcmd):
response = "ADC1="
response += (" "+str(self.lastFilamentWidthReading))
response += (" ADC2="+str(self.lastFilamentWidthReading2))
response += (" RAW="+
str(self.lastFilamentWidthReading
+self.lastFilamentWidthReading2))
gcmd.respond_info(response)
def get_status(self, eventtime):
return {'Diameter': self.diameter,
'Raw':(self.lastFilamentWidthReading+
self.lastFilamentWidthReading2),
'is_active':self.is_active}
def cmd_log_enable(self, gcmd):
self.is_log = True
gcmd.respond_info("Filament width logging Turned On")
def cmd_log_disable(self, gcmd):
self.is_log = False
gcmd.respond_info("Filament width logging Turned Off")
def load_config(config):
return HallFilamentWidthSensor(config)

View File

@@ -0,0 +1,28 @@
# Support for a heated bed
#
# Copyright (C) 2018-2019 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class PrinterHeaterBed:
def __init__(self, config):
self.printer = config.get_printer()
pheaters = self.printer.load_object(config, 'heaters')
self.heater = pheaters.setup_heater(config, 'B')
self.get_status = self.heater.get_status
self.stats = self.heater.stats
# Register commands
gcode = self.printer.lookup_object('gcode')
gcode.register_command("M140", self.cmd_M140)
gcode.register_command("M190", self.cmd_M190)
def cmd_M140(self, gcmd, wait=False):
# Set Bed Temperature
temp = gcmd.get_float('S', 0.)
pheaters = self.printer.lookup_object('heaters')
pheaters.set_temperature(self.heater, temp, wait)
def cmd_M190(self, gcmd):
# Set Bed Temperature and Wait
self.cmd_M140(gcmd, wait=True)
def load_config(config):
return PrinterHeaterBed(config)

Binary file not shown.

View File

@@ -0,0 +1,42 @@
# Support fans that are enabled when a heater is on
#
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import fan
PIN_MIN_TIME = 0.100
class PrinterHeaterFan:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.load_object(config, 'heaters')
self.printer.register_event_handler("klippy:ready", self.handle_ready)
self.heater_names = config.getlist("heater", ("extruder",))
self.heater_temp = config.getfloat("heater_temp", 50.0)
self.heaters = []
self.fan = fan.Fan(config, default_shutdown_speed=1.)
self.fan_speed = config.getfloat("fan_speed", 1., minval=0., maxval=1.)
self.last_speed = 0.
def handle_ready(self):
pheaters = self.printer.lookup_object('heaters')
self.heaters = [pheaters.lookup_heater(n) for n in self.heater_names]
reactor = self.printer.get_reactor()
reactor.register_timer(self.callback, reactor.monotonic()+PIN_MIN_TIME)
def get_status(self, eventtime):
return self.fan.get_status(eventtime)
def callback(self, eventtime):
speed = 0.
for heater in self.heaters:
current_temp, target_temp = heater.get_temp(eventtime)
if target_temp or current_temp > self.heater_temp:
speed = self.fan_speed
if speed != self.last_speed:
self.last_speed = speed
curtime = self.printer.get_reactor().monotonic()
print_time = self.fan.get_mcu().estimated_print_time(curtime)
self.fan.set_speed(print_time + PIN_MIN_TIME, speed)
return eventtime + 1.
def load_config_prefix(config):
return PrinterHeaterFan(config)

Binary file not shown.

View File

@@ -0,0 +1,9 @@
# Support for a generic heater
#
# Copyright (C) 2019 John Jardine <john@gprime.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
def load_config_prefix(config):
pheaters = config.get_printer().load_object(config, 'heaters')
return pheaters.setup_heater(config)

Binary file not shown.

371
klippy/extras/heaters.py Normal file
View File

@@ -0,0 +1,371 @@
# Tracking of PWM controlled heaters and their temperature control
#
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import os, logging, threading
######################################################################
# Heater
######################################################################
KELVIN_TO_CELSIUS = -273.15
MAX_HEAT_TIME = 5.0
AMBIENT_TEMP = 25.
PID_PARAM_BASE = 255.
class Heater:
def __init__(self, config, sensor):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
# Setup sensor
self.sensor = sensor
self.min_temp = config.getfloat('min_temp', minval=KELVIN_TO_CELSIUS)
self.max_temp = config.getfloat('max_temp', above=self.min_temp)
self.sensor.setup_minmax(self.min_temp, self.max_temp)
self.sensor.setup_callback(self.temperature_callback)
self.pwm_delay = self.sensor.get_report_time_delta()
# Setup temperature checks
self.min_extrude_temp = config.getfloat(
'min_extrude_temp', 170.,
minval=self.min_temp, maxval=self.max_temp)
is_fileoutput = (self.printer.get_start_args().get('debugoutput')
is not None)
self.can_extrude = self.min_extrude_temp <= 0. or is_fileoutput
self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.)
self.smooth_time = config.getfloat('smooth_time', 1., above=0.)
self.inv_smooth_time = 1. / self.smooth_time
self.lock = threading.Lock()
self.last_temp = self.smoothed_temp = self.target_temp = 0.
self.last_temp_time = 0.
# pwm caching
self.next_pwm_time = 0.
self.last_pwm_value = 0.
# Setup control algorithm sub-class
algos = {'watermark': ControlBangBang, 'pid': ControlPID}
algo = config.getchoice('control', algos)
self.control = algo(self, config)
# Setup output heater pin
heater_pin = config.get('heater_pin')
ppins = self.printer.lookup_object('pins')
self.mcu_pwm = ppins.setup_pin('pwm', heater_pin)
pwm_cycle_time = config.getfloat('pwm_cycle_time', 0.100, above=0.,
maxval=self.pwm_delay)
self.mcu_pwm.setup_cycle_time(pwm_cycle_time)
self.mcu_pwm.setup_max_duration(MAX_HEAT_TIME)
# Load additional modules
self.printer.load_object(config, "verify_heater %s" % (self.name,))
self.printer.load_object(config, "pid_calibrate")
gcode = self.printer.lookup_object("gcode")
gcode.register_mux_command("SET_HEATER_TEMPERATURE", "HEATER",
self.name, self.cmd_SET_HEATER_TEMPERATURE,
desc=self.cmd_SET_HEATER_TEMPERATURE_help)
def set_pwm(self, read_time, value):
if self.target_temp <= 0.:
value = 0.
if ((read_time < self.next_pwm_time or not self.last_pwm_value)
and abs(value - self.last_pwm_value) < 0.05):
# No significant change in value - can suppress update
return
pwm_time = read_time + self.pwm_delay
self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME
self.last_pwm_value = value
self.mcu_pwm.set_pwm(pwm_time, value)
#logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])",
# self.name, value, pwm_time,
# self.last_temp, self.last_temp_time, self.target_temp)
def temperature_callback(self, read_time, temp):
with self.lock:
time_diff = read_time - self.last_temp_time
self.last_temp = temp
self.last_temp_time = read_time
self.control.temperature_update(read_time, temp, self.target_temp)
temp_diff = temp - self.smoothed_temp
adj_time = min(time_diff * self.inv_smooth_time, 1.)
self.smoothed_temp += temp_diff * adj_time
self.can_extrude = (self.smoothed_temp >= self.min_extrude_temp)
#logging.debug("temp: %.3f %f = %f", read_time, temp)
# External commands
def get_pwm_delay(self):
return self.pwm_delay
def get_max_power(self):
return self.max_power
def get_smooth_time(self):
return self.smooth_time
def set_temp(self, degrees):
if degrees and (degrees < self.min_temp or degrees > self.max_temp):
raise self.printer.command_error(
"Requested temperature (%.1f) out of range (%.1f:%.1f)"
% (degrees, self.min_temp, self.max_temp))
with self.lock:
self.target_temp = degrees
def get_temp(self, eventtime):
print_time = self.mcu_pwm.get_mcu().estimated_print_time(eventtime) - 5.
with self.lock:
if self.last_temp_time < print_time:
return 0., self.target_temp
return self.smoothed_temp, self.target_temp
def check_busy(self, eventtime):
with self.lock:
return self.control.check_busy(
eventtime, self.smoothed_temp, self.target_temp)
def set_control(self, control):
with self.lock:
old_control = self.control
self.control = control
self.target_temp = 0.
return old_control
def alter_target(self, target_temp):
if target_temp:
target_temp = max(self.min_temp, min(self.max_temp, target_temp))
self.target_temp = target_temp
def stats(self, eventtime):
with self.lock:
target_temp = self.target_temp
last_temp = self.last_temp
last_pwm_value = self.last_pwm_value
is_active = target_temp or last_temp > 50.
return is_active, '%s: target=%.0f temp=%.1f pwm=%.3f' % (
self.name, target_temp, last_temp, last_pwm_value)
def get_status(self, eventtime):
with self.lock:
target_temp = self.target_temp
smoothed_temp = self.smoothed_temp
last_pwm_value = self.last_pwm_value
return {'temperature': round(smoothed_temp, 2), 'target': target_temp,
'power': last_pwm_value}
cmd_SET_HEATER_TEMPERATURE_help = "Sets a heater temperature"
def cmd_SET_HEATER_TEMPERATURE(self, gcmd):
temp = gcmd.get_float('TARGET', 0.)
pheaters = self.printer.lookup_object('heaters')
pheaters.set_temperature(self, temp)
######################################################################
# Bang-bang control algo
######################################################################
class ControlBangBang:
def __init__(self, heater, config):
self.heater = heater
self.heater_max_power = heater.get_max_power()
self.max_delta = config.getfloat('max_delta', 2.0, above=0.)
self.heating = False
def temperature_update(self, read_time, temp, target_temp):
if self.heating and temp >= target_temp+self.max_delta:
self.heating = False
elif not self.heating and temp <= target_temp-self.max_delta:
self.heating = True
if self.heating:
self.heater.set_pwm(read_time, self.heater_max_power)
else:
self.heater.set_pwm(read_time, 0.)
def check_busy(self, eventtime, smoothed_temp, target_temp):
return smoothed_temp < target_temp-self.max_delta
######################################################################
# Proportional Integral Derivative (PID) control algo
######################################################################
PID_SETTLE_DELTA = 1.
PID_SETTLE_SLOPE = .1
class ControlPID:
def __init__(self, heater, config):
self.heater = heater
self.heater_max_power = heater.get_max_power()
self.Kp = config.getfloat('pid_Kp') / PID_PARAM_BASE
self.Ki = config.getfloat('pid_Ki') / PID_PARAM_BASE
self.Kd = config.getfloat('pid_Kd') / PID_PARAM_BASE
self.min_deriv_time = heater.get_smooth_time()
self.temp_integ_max = 0.
if self.Ki:
self.temp_integ_max = self.heater_max_power / self.Ki
self.prev_temp = AMBIENT_TEMP
self.prev_temp_time = 0.
self.prev_temp_deriv = 0.
self.prev_temp_integ = 0.
def temperature_update(self, read_time, temp, target_temp):
time_diff = read_time - self.prev_temp_time
# Calculate change of temperature
temp_diff = temp - self.prev_temp
if time_diff >= self.min_deriv_time:
temp_deriv = temp_diff / time_diff
else:
temp_deriv = (self.prev_temp_deriv * (self.min_deriv_time-time_diff)
+ temp_diff) / self.min_deriv_time
# Calculate accumulated temperature "error"
temp_err = target_temp - temp
temp_integ = self.prev_temp_integ + temp_err * time_diff
temp_integ = max(0., min(self.temp_integ_max, temp_integ))
# Calculate output
co = self.Kp*temp_err + self.Ki*temp_integ - self.Kd*temp_deriv
#logging.debug("pid: %f@%.3f -> diff=%f deriv=%f err=%f integ=%f co=%d",
# temp, read_time, temp_diff, temp_deriv, temp_err, temp_integ, co)
bounded_co = max(0., min(self.heater_max_power, co))
self.heater.set_pwm(read_time, bounded_co)
# Store state for next measurement
self.prev_temp = temp
self.prev_temp_time = read_time
self.prev_temp_deriv = temp_deriv
if co == bounded_co:
self.prev_temp_integ = temp_integ
def check_busy(self, eventtime, smoothed_temp, target_temp):
temp_diff = target_temp - smoothed_temp
return (abs(temp_diff) > PID_SETTLE_DELTA
or abs(self.prev_temp_deriv) > PID_SETTLE_SLOPE)
######################################################################
# Sensor and heater lookup
######################################################################
class PrinterHeaters:
def __init__(self, config):
self.printer = config.get_printer()
self.sensor_factories = {}
self.heaters = {}
self.gcode_id_to_sensor = {}
self.available_heaters = []
self.available_sensors = []
self.has_started = self.have_load_sensors = False
self.printer.register_event_handler("klippy:ready", self._handle_ready)
self.printer.register_event_handler("gcode:request_restart",
self.turn_off_all_heaters)
# Register commands
gcode = self.printer.lookup_object('gcode')
gcode.register_command("TURN_OFF_HEATERS", self.cmd_TURN_OFF_HEATERS,
desc=self.cmd_TURN_OFF_HEATERS_help)
gcode.register_command("M105", self.cmd_M105, when_not_ready=True)
gcode.register_command("TEMPERATURE_WAIT", self.cmd_TEMPERATURE_WAIT,
desc=self.cmd_TEMPERATURE_WAIT_help)
def load_config(self, config):
self.have_load_sensors = True
# Load default temperature sensors
pconfig = self.printer.lookup_object('configfile')
dir_name = os.path.dirname(__file__)
filename = os.path.join(dir_name, 'temperature_sensors.cfg')
try:
dconfig = pconfig.read_config(filename)
except Exception:
raise config.config_error("Cannot load config '%s'" % (filename,))
for c in dconfig.get_prefix_sections(''):
self.printer.load_object(dconfig, c.get_name())
def add_sensor_factory(self, sensor_type, sensor_factory):
self.sensor_factories[sensor_type] = sensor_factory
def setup_heater(self, config, gcode_id=None):
heater_name = config.get_name().split()[-1]
if heater_name in self.heaters:
raise config.error("Heater %s already registered" % (heater_name,))
# Setup sensor
sensor = self.setup_sensor(config)
# Create heater
self.heaters[heater_name] = heater = Heater(config, sensor)
self.register_sensor(config, heater, gcode_id)
self.available_heaters.append(config.get_name())
return heater
def get_all_heaters(self):
return self.available_heaters
def lookup_heater(self, heater_name):
if heater_name not in self.heaters:
raise self.printer.config_error(
"Unknown heater '%s'" % (heater_name,))
return self.heaters[heater_name]
def setup_sensor(self, config):
if not self.have_load_sensors:
self.load_config(config)
sensor_type = config.get('sensor_type')
if sensor_type not in self.sensor_factories:
raise self.printer.config_error(
"Unknown temperature sensor '%s'" % (sensor_type,))
if sensor_type == 'NTC 100K beta 3950':
config.deprecate('sensor_type', 'NTC 100K beta 3950')
return self.sensor_factories[sensor_type](config)
def register_sensor(self, config, psensor, gcode_id=None):
self.available_sensors.append(config.get_name())
if gcode_id is None:
gcode_id = config.get('gcode_id', None)
if gcode_id is None:
return
if gcode_id in self.gcode_id_to_sensor:
raise self.printer.config_error(
"G-Code sensor id %s already registered" % (gcode_id,))
self.gcode_id_to_sensor[gcode_id] = psensor
def get_status(self, eventtime):
return {'available_heaters': self.available_heaters,
'available_sensors': self.available_sensors}
def turn_off_all_heaters(self, print_time=0.):
for heater in self.heaters.values():
heater.set_temp(0.)
cmd_TURN_OFF_HEATERS_help = "Turn off all heaters"
def cmd_TURN_OFF_HEATERS(self, gcmd):
self.turn_off_all_heaters()
# G-Code M105 temperature reporting
def _handle_ready(self):
self.has_started = True
def _get_temp(self, eventtime):
# Tn:XXX /YYY B:XXX /YYY
out = []
if self.has_started:
for gcode_id, sensor in sorted(self.gcode_id_to_sensor.items()):
cur, target = sensor.get_temp(eventtime)
out.append("%s:%.1f /%.1f" % (gcode_id, cur, target))
if not out:
return "T:0"
return " ".join(out)
def cmd_M105(self, gcmd):
# Get Extruder Temperature
reactor = self.printer.get_reactor()
msg = self._get_temp(reactor.monotonic())
did_ack = gcmd.ack(msg)
if not did_ack:
gcmd.respond_raw(msg)
def _wait_for_temperature(self, heater):
# Helper to wait on heater.check_busy() and report M105 temperatures
if self.printer.get_start_args().get('debugoutput') is not None:
return
toolhead = self.printer.lookup_object("toolhead")
gcode = self.printer.lookup_object("gcode")
reactor = self.printer.get_reactor()
eventtime = reactor.monotonic()
while not self.printer.is_shutdown() and heater.check_busy(eventtime):
print_time = toolhead.get_last_move_time()
gcode.respond_raw(self._get_temp(eventtime))
eventtime = reactor.pause(eventtime + 1.)
def set_temperature(self, heater, temp, wait=False):
toolhead = self.printer.lookup_object('toolhead')
toolhead.register_lookahead_callback((lambda pt: None))
heater.set_temp(temp)
if wait and temp:
self._wait_for_temperature(heater)
cmd_TEMPERATURE_WAIT_help = "Wait for a temperature on a sensor"
def cmd_TEMPERATURE_WAIT(self, gcmd):
sensor_name = gcmd.get('SENSOR')
if sensor_name not in self.available_sensors:
raise gcmd.error("Unknown sensor '%s'" % (sensor_name,))
min_temp = gcmd.get_float('MINIMUM', float('-inf'))
max_temp = gcmd.get_float('MAXIMUM', float('inf'), above=min_temp)
if min_temp == float('-inf') and max_temp == float('inf'):
raise gcmd.error(
"Error on 'TEMPERATURE_WAIT': missing MINIMUM or MAXIMUM.")
if self.printer.get_start_args().get('debugoutput') is not None:
return
if sensor_name in self.heaters:
sensor = self.heaters[sensor_name]
else:
sensor = self.printer.lookup_object(sensor_name)
toolhead = self.printer.lookup_object("toolhead")
reactor = self.printer.get_reactor()
eventtime = reactor.monotonic()
while not self.printer.is_shutdown():
temp, target = sensor.get_temp(eventtime)
if temp >= min_temp and temp <= max_temp:
return
print_time = toolhead.get_last_move_time()
gcmd.respond_raw(self._get_temp(eventtime))
eventtime = reactor.pause(eventtime + 1.)
def load_config(config):
return PrinterHeaters(config)

BIN
klippy/extras/heaters.pyc Normal file

Binary file not shown.

277
klippy/extras/homing.py Normal file
View File

@@ -0,0 +1,277 @@
# Helper code for implementing homing operations
#
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math
HOMING_START_DELAY = 0.001
ENDSTOP_SAMPLE_TIME = .000015
ENDSTOP_SAMPLE_COUNT = 4
# Return a completion that completes when all completions in a list complete
def multi_complete(printer, completions):
if len(completions) == 1:
return completions[0]
# Build completion that waits for all completions
reactor = printer.get_reactor()
cp = reactor.register_callback(lambda e: [c.wait() for c in completions])
# If any completion indicates an error, then exit main completion early
for c in completions:
reactor.register_callback(
lambda e, c=c: cp.complete(1) if c.wait() else 0)
return cp
# Tracking of stepper positions during a homing/probing move
class StepperPosition:
def __init__(self, stepper, endstop_name):
self.stepper = stepper
self.endstop_name = endstop_name
self.stepper_name = stepper.get_name()
self.start_pos = stepper.get_mcu_position()
self.halt_pos = self.trig_pos = None
def note_home_end(self, trigger_time):
self.halt_pos = self.stepper.get_mcu_position()
self.trig_pos = self.stepper.get_past_mcu_position(trigger_time)
# Implementation of homing/probing moves
class HomingMove:
def __init__(self, printer, endstops, toolhead=None):
self.printer = printer
self.endstops = endstops
if toolhead is None:
toolhead = printer.lookup_object('toolhead')
self.toolhead = toolhead
self.stepper_positions = []
def get_mcu_endstops(self):
return [es for es, name in self.endstops]
def _calc_endstop_rate(self, mcu_endstop, movepos, speed):
startpos = self.toolhead.get_position()
axes_d = [mp - sp for mp, sp in zip(movepos, startpos)]
move_d = math.sqrt(sum([d*d for d in axes_d[:3]]))
move_t = move_d / speed
max_steps = max([(abs(s.calc_position_from_coord(startpos)
- s.calc_position_from_coord(movepos))
/ s.get_step_dist())
for s in mcu_endstop.get_steppers()])
if max_steps <= 0.:
return .001
return move_t / max_steps
def calc_toolhead_pos(self, kin_spos, offsets):
kin_spos = dict(kin_spos)
kin = self.toolhead.get_kinematics()
for stepper in kin.get_steppers():
sname = stepper.get_name()
kin_spos[sname] += offsets.get(sname, 0) * stepper.get_step_dist()
thpos = self.toolhead.get_position()
return list(kin.calc_position(kin_spos))[:3] + thpos[3:]
def homing_move(self, movepos, speed, probe_pos=False,
triggered=True, check_triggered=True):
# Notify start of homing/probing move
self.printer.send_event("homing:homing_move_begin", self)
# Note start location
self.toolhead.flush_step_generation()
kin = self.toolhead.get_kinematics()
kin_spos = {s.get_name(): s.get_commanded_position()
for s in kin.get_steppers()}
self.stepper_positions = [ StepperPosition(s, name)
for es, name in self.endstops
for s in es.get_steppers() ]
# Start endstop checking
print_time = self.toolhead.get_last_move_time()
endstop_triggers = []
for mcu_endstop, name in self.endstops:
rest_time = self._calc_endstop_rate(mcu_endstop, movepos, speed)
wait = mcu_endstop.home_start(print_time, ENDSTOP_SAMPLE_TIME,
ENDSTOP_SAMPLE_COUNT, rest_time,
triggered=triggered)
endstop_triggers.append(wait)
all_endstop_trigger = multi_complete(self.printer, endstop_triggers)
self.toolhead.dwell(HOMING_START_DELAY)
# Issue move
error = None
try:
self.toolhead.drip_move(movepos, speed, all_endstop_trigger)
except self.printer.command_error as e:
error = "Error during homing move: %s" % (str(e),)
# Wait for endstops to trigger
trigger_times = {}
move_end_print_time = self.toolhead.get_last_move_time()
for mcu_endstop, name in self.endstops:
trigger_time = mcu_endstop.home_wait(move_end_print_time)
if trigger_time > 0.:
trigger_times[name] = trigger_time
elif trigger_time < 0. and error is None:
error = "Communication timeout during homing %s" % (name,)
elif check_triggered and error is None:
error = "No trigger on %s after full movement" % (name,)
# Determine stepper halt positions
self.toolhead.flush_step_generation()
for sp in self.stepper_positions:
tt = trigger_times.get(sp.endstop_name, move_end_print_time)
sp.note_home_end(tt)
if probe_pos:
halt_steps = {sp.stepper_name: sp.halt_pos - sp.start_pos
for sp in self.stepper_positions}
trig_steps = {sp.stepper_name: sp.trig_pos - sp.start_pos
for sp in self.stepper_positions}
haltpos = trigpos = self.calc_toolhead_pos(kin_spos, trig_steps)
if trig_steps != halt_steps:
haltpos = self.calc_toolhead_pos(kin_spos, halt_steps)
else:
haltpos = trigpos = movepos
over_steps = {sp.stepper_name: sp.halt_pos - sp.trig_pos
for sp in self.stepper_positions}
if any(over_steps.values()):
self.toolhead.set_position(movepos)
halt_kin_spos = {s.get_name(): s.get_commanded_position()
for s in kin.get_steppers()}
haltpos = self.calc_toolhead_pos(halt_kin_spos, over_steps)
self.toolhead.set_position(haltpos)
# Signal homing/probing move complete
try:
self.printer.send_event("homing:homing_move_end", self)
except self.printer.command_error as e:
if error is None:
error = str(e)
if error is not None:
raise self.printer.command_error(error)
return trigpos
def check_no_movement(self):
if self.printer.get_start_args().get('debuginput') is not None:
return None
for sp in self.stepper_positions:
if sp.start_pos == sp.trig_pos:
return sp.endstop_name
return None
# State tracking of homing requests
class Homing:
def __init__(self, printer):
self.printer = printer
self.toolhead = printer.lookup_object('toolhead')
self.changed_axes = []
self.trigger_mcu_pos = {}
self.adjust_pos = {}
def set_axes(self, axes):
self.changed_axes = axes
def get_axes(self):
return self.changed_axes
def get_trigger_position(self, stepper_name):
return self.trigger_mcu_pos[stepper_name]
def set_stepper_adjustment(self, stepper_name, adjustment):
self.adjust_pos[stepper_name] = adjustment
def _fill_coord(self, coord):
# Fill in any None entries in 'coord' with current toolhead position
thcoord = list(self.toolhead.get_position())
for i in range(len(coord)):
if coord[i] is not None:
thcoord[i] = coord[i]
return thcoord
def set_homed_position(self, pos):
self.toolhead.set_position(self._fill_coord(pos))
def home_rails(self, rails, forcepos, movepos):
# Notify of upcoming homing operation
self.printer.send_event("homing:home_rails_begin", self, rails)
# Alter kinematics class to think printer is at forcepos
homing_axes = [axis for axis in range(3) if forcepos[axis] is not None]
startpos = self._fill_coord(forcepos)
homepos = self._fill_coord(movepos)
self.toolhead.set_position(startpos, homing_axes=homing_axes)
# Perform first home
endstops = [es for rail in rails for es in rail.get_endstops()]
hi = rails[0].get_homing_info()
hmove = HomingMove(self.printer, endstops)
hmove.homing_move(homepos, hi.speed)
# Perform second home
if hi.retract_dist:
# Retract
startpos = self._fill_coord(forcepos)
homepos = self._fill_coord(movepos)
axes_d = [hp - sp for hp, sp in zip(homepos, startpos)]
move_d = math.sqrt(sum([d*d for d in axes_d[:3]]))
retract_r = min(1., hi.retract_dist / move_d)
retractpos = [hp - ad * retract_r
for hp, ad in zip(homepos, axes_d)]
self.toolhead.move(retractpos, hi.retract_speed)
# Home again
startpos = [rp - ad * retract_r
for rp, ad in zip(retractpos, axes_d)]
self.toolhead.set_position(startpos)
hmove = HomingMove(self.printer, endstops)
hmove.homing_move(homepos, hi.second_homing_speed)
if hmove.check_no_movement() is not None:
raise self.printer.command_error(
"Endstop %s still triggered after retract"
% (hmove.check_no_movement(),))
# Signal home operation complete
self.toolhead.flush_step_generation()
self.trigger_mcu_pos = {sp.stepper_name: sp.trig_pos
for sp in hmove.stepper_positions}
self.adjust_pos = {}
self.printer.send_event("homing:home_rails_end", self, rails)
if any(self.adjust_pos.values()):
# Apply any homing offsets
kin = self.toolhead.get_kinematics()
homepos = self.toolhead.get_position()
kin_spos = {s.get_name(): (s.get_commanded_position()
+ self.adjust_pos.get(s.get_name(), 0.))
for s in kin.get_steppers()}
newpos = kin.calc_position(kin_spos)
for axis in homing_axes:
homepos[axis] = newpos[axis]
self.toolhead.set_position(homepos)
class PrinterHoming:
def __init__(self, config):
self.printer = config.get_printer()
# Register g-code commands
gcode = self.printer.lookup_object('gcode')
gcode.register_command('G28', self.cmd_G28)
def manual_home(self, toolhead, endstops, pos, speed,
triggered, check_triggered):
hmove = HomingMove(self.printer, endstops, toolhead)
try:
hmove.homing_move(pos, speed, triggered=triggered,
check_triggered=check_triggered)
except self.printer.command_error:
if self.printer.is_shutdown():
raise self.printer.command_error(
"Homing failed due to printer shutdown")
raise
def probing_move(self, mcu_probe, pos, speed):
endstops = [(mcu_probe, "probe")]
hmove = HomingMove(self.printer, endstops)
try:
epos = hmove.homing_move(pos, speed, probe_pos=True)
except self.printer.command_error:
if self.printer.is_shutdown():
raise self.printer.command_error(
"Probing failed due to printer shutdown")
raise
if hmove.check_no_movement() is not None:
raise self.printer.command_error(
"Probe triggered prior to movement")
return epos
def cmd_G28(self, gcmd):
# Move to origin
axes = []
for pos, axis in enumerate('XYZ'):
if gcmd.get(axis, None) is not None:
axes.append(pos)
if not axes:
axes = [0, 1, 2]
homing_state = Homing(self.printer)
homing_state.set_axes(axes)
kin = self.printer.lookup_object('toolhead').get_kinematics()
try:
kin.home(homing_state)
except self.printer.command_error:
if self.printer.is_shutdown():
raise self.printer.command_error(
"Homing failed due to printer shutdown")
self.printer.lookup_object('stepper_enable').motor_off()
raise
def load_config(config):
return PrinterHoming(config)

BIN
klippy/extras/homing.pyc Normal file

Binary file not shown.

View File

@@ -0,0 +1,64 @@
# Heater handling on homing moves
#
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
class HomingHeaters:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.printer.register_event_handler("homing:homing_move_begin",
self.handle_homing_move_begin)
self.printer.register_event_handler("homing:homing_move_end",
self.handle_homing_move_end)
self.disable_heaters = config.getlist("heaters", None)
self.flaky_steppers = config.getlist("steppers", None)
self.pheaters = self.printer.load_object(config, 'heaters')
self.target_save = {}
def handle_connect(self):
# heaters to disable
all_heaters = self.pheaters.get_all_heaters()
if self.disable_heaters is None:
self.disable_heaters = all_heaters
else:
if not all(x in all_heaters for x in self.disable_heaters):
raise self.printer.config_error(
"One or more of these heaters are unknown: %s"
% (self.disable_heaters,))
# steppers valid?
kin = self.printer.lookup_object('toolhead').get_kinematics()
all_steppers = [s.get_name() for s in kin.get_steppers()]
if self.flaky_steppers is None:
return
if not all(x in all_steppers for x in self.flaky_steppers):
raise self.printer.config_error(
"One or more of these steppers are unknown: %s"
% (self.flaky_steppers,))
def check_eligible(self, endstops):
if self.flaky_steppers is None:
return True
steppers_being_homed = [s.get_name()
for es in endstops
for s in es.get_steppers()]
return any(x in self.flaky_steppers for x in steppers_being_homed)
def handle_homing_move_begin(self, hmove):
if not self.check_eligible(hmove.get_mcu_endstops()):
return
for heater_name in self.disable_heaters:
heater = self.pheaters.lookup_heater(heater_name)
self.target_save[heater_name] = heater.get_temp(0)[1]
heater.set_temp(0.)
def handle_homing_move_end(self, hmove):
if not self.check_eligible(hmove.get_mcu_endstops()):
return
for heater_name in self.disable_heaters:
heater = self.pheaters.lookup_heater(heater_name)
heater.set_temp(self.target_save[heater_name])
def load_config(config):
return HomingHeaters(config)

View File

@@ -0,0 +1,65 @@
# Run user defined actions in place of a normal G28 homing command
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class HomingOverride:
def __init__(self, config):
self.printer = config.get_printer()
self.start_pos = [config.getfloat('set_position_' + a, None)
for a in 'xyz']
self.axes = config.get('axes', 'XYZ').upper()
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.template = gcode_macro.load_template(config, 'gcode')
self.in_script = False
self.printer.load_object(config, 'homing')
self.gcode = self.printer.lookup_object('gcode')
self.prev_G28 = self.gcode.register_command("G28", None)
self.gcode.register_command("G28", self.cmd_G28)
def cmd_G28(self, gcmd):
if self.in_script:
# Was called recursively - invoke the real G28 command
self.prev_G28(gcmd)
return
# if no axis is given as parameter we assume the override
no_axis = True
for axis in 'XYZ':
if gcmd.get(axis, None) is not None:
no_axis = False
break
if no_axis:
override = True
else:
# check if we home an axis which needs the override
override = False
for axis in self.axes:
if gcmd.get(axis, None) is not None:
override = True
if not override:
self.prev_G28(gcmd)
return
# Calculate forced position (if configured)
toolhead = self.printer.lookup_object('toolhead')
pos = toolhead.get_position()
homing_axes = []
for axis, loc in enumerate(self.start_pos):
if loc is not None:
pos[axis] = loc
homing_axes.append(axis)
toolhead.set_position(pos, homing_axes=homing_axes)
# Perform homing
context = self.template.create_template_context()
context['params'] = gcmd.get_command_parameters()
try:
self.in_script = True
self.template.run_gcode_from_command(context)
finally:
self.in_script = False
def load_config(config):
return HomingOverride(config)

Binary file not shown.

248
klippy/extras/htu21d.py Normal file
View File

@@ -0,0 +1,248 @@
# HTU21D(F)/Si7013/Si7020/Si7021/SHT21 i2c based temperature sensors support
#
# Copyright (C) 2020 Lucio Tarantino <lucio.tarantino@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import bus
######################################################################
# NOTE: The implementation requires write support of length 0
# before reading on the i2c bus of the mcu.
#
# Compatible Sensors:
# HTU21D - Tested on Linux MCU.
# Si7013 - Untested
# Si7020 - Untested
# Si7021 - Tested on Pico MCU
# SHT21 - Untested
#
######################################################################
HTU21D_I2C_ADDR= 0x40
HTU21D_COMMANDS = {
'HTU21D_TEMP' :0xE3,
'HTU21D_HUMID' :0xE5,
'HTU21D_TEMP_NH' :0xF3,
'HTU21D_HUMID_NH' :0xF5,
'WRITE' :0xE6,
'READ' :0xE7,
'RESET' :0xFE,
'SERIAL' :[0xFA,0x0F,0xFC,0xC9],
'FIRMWARE_READ' :[0x84,0xB8]
}
HTU21D_RESOLUTION_MASK = 0x7E;
HTU21D_RESOLUTIONS = {
'TEMP14_HUM12':int('00000000',2),
'TEMP13_HUM10':int('10000000',2),
'TEMP12_HUM08':int('00000001',2),
'TEMP11_HUM11':int('10000001',2)
}
# Device with conversion time for tmp/resolution bit
# The format is:
# <CHIPNAME>:{id:<ID>, ..<RESOlUTION>:[<temp time>,<humidity time>].. }
HTU21D_DEVICES = {
'SI7013':{'id':0x0D,
'TEMP14_HUM12':[.11,.12],
'TEMP13_HUM10':[ .7, .5],
'TEMP12_HUM08':[ .4, .4],
'TEMP11_HUM11':[ .3, .7]},
'SI7020':{'id':0x14,
'TEMP14_HUM12':[.11,.12],
'TEMP13_HUM10':[ .7, .5],
'TEMP12_HUM08':[ .4, .4],
'TEMP11_HUM11':[ .3, .7]},
'SI7021':{'id':0x15,
'TEMP14_HUM12':[.11,.12],
'TEMP13_HUM10':[ .7, .5],
'TEMP12_HUM08':[ .4, .4],
'TEMP11_HUM11':[ .3, .7]},
'SHT21': {'id':0x31,
'TEMP14_HUM12':[.85,.29],
'TEMP13_HUM10':[.43, .9],
'TEMP12_HUM08':[.22, .4],
'TEMP11_HUM11':[.11,.15]},
'HTU21D':{'id':0x32,
'TEMP14_HUM12':[.50,.16],
'TEMP13_HUM10':[.25, .5],
'TEMP12_HUM08':[.13, .3],
'TEMP11_HUM11':[.12, .8]}
}
#temperature coefficient for RH compensation at range 0C..80C,
# for HTU21D & SHT21 only
HTU21D_TEMP_COEFFICIENT= -0.15
#crc8 polynomial for 16bit value, CRC8 -> x^8 + x^5 + x^4 + 1
HTU21D_CRC8_POLYNOMINAL= 0x13100
class HTU21D:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.reactor = self.printer.get_reactor()
self.i2c = bus.MCU_I2C_from_config(
config, default_addr=HTU21D_I2C_ADDR, default_speed=100000)
self.hold_master_mode = config.getboolean('htu21d_hold_master',False)
self.resolution = config.get('htu21d_resolution','TEMP12_HUM08')
self.report_time = config.getint('htu21d_report_time',30,minval=5)
if self.resolution not in HTU21D_RESOLUTIONS:
raise config.error("Invalid HTU21D Resolution. Valid are %s"
% '|'.join(HTU21D_RESOLUTIONS.keys()))
self.deviceId = config.get('sensor_type')
self.temp = self.min_temp = self.max_temp = self.humidity = 0.
self.sample_timer = self.reactor.register_timer(self._sample_htu21d)
self.printer.add_object("htu21d " + self.name, self)
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
def handle_connect(self):
self._init_htu21d()
self.reactor.update_timer(self.sample_timer, self.reactor.NOW)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
def setup_callback(self, cb):
self._callback = cb
def get_report_time_delta(self):
return self.report_time
def _init_htu21d(self):
# Device Soft Reset
self.i2c.i2c_write([HTU21D_COMMANDS['RESET']])
# Wait 15ms after reset
self.reactor.pause(self.reactor.monotonic() + .15)
# Read ChipId
params = self.i2c.i2c_read([HTU21D_COMMANDS['SERIAL'][2],
HTU21D_COMMANDS['SERIAL'][3]], 3)
response = bytearray(params['response'])
rdevId = response[0] << 8
rdevId |= response[1]
checksum = response[2]
if self._chekCRC8(rdevId) != checksum:
logging.warn("htu21d: Reading deviceId !Checksum error!")
rdevId = rdevId >> 8
deviceId_list = list(
filter(
lambda elem: HTU21D_DEVICES[elem]['id'] == rdevId,HTU21D_DEVICES)
)
if len(deviceId_list) != 0:
logging.info("htu21d: Found Device Type %s" % deviceId_list[0])
else:
logging.warn("htu21d: Unknown Device ID %#x " % rdevId)
if(self.deviceId != deviceId_list[0]):
logging.warn(
"htu21d: Found device %s. Forcing to type %s as config.",
deviceId_list[0],self.deviceId)
# Set Resolution
params = self.i2c.i2c_read([HTU21D_COMMANDS['READ']], 1)
response = bytearray(params['response'])
registerData = response[0] & HTU21D_RESOLUTION_MASK
registerData |= HTU21D_RESOLUTIONS[self.resolution]
self.i2c.i2c_write([HTU21D_COMMANDS['WRITE']],registerData)
logging.info("htu21d: Setting resolution to %s " % self.resolution)
def _sample_htu21d(self, eventtime):
try:
# Read Temeprature
if self.hold_master_mode:
params = self.i2c.i2c_write([HTU21D_COMMANDS['HTU21D_TEMP']])
else:
params = self.i2c.i2c_write([HTU21D_COMMANDS['HTU21D_TEMP_NH']])
# Wait
self.reactor.pause(self.reactor.monotonic()
+ HTU21D_DEVICES[self.deviceId][self.resolution][0])
params = self.i2c.i2c_read([],3)
response = bytearray(params['response'])
rtemp = response[0] << 8
rtemp |= response[1]
if self._chekCRC8(rtemp) != response[2]:
logging.warn("htu21d: Checksum error on Temperature reading!")
else:
self.temp = (0.002681 * float(rtemp) - 46.85)
logging.debug("htu21d: Temperature %.2f " % self.temp)
# Read Humidity
if self.hold_master_mode:
self.i2c.i2c_write([HTU21D_COMMANDS['HTU21D_HUMID']])
else:
self.i2c.i2c_write([HTU21D_COMMANDS['HTU21D_HUMID_NH']])
# Wait
self.reactor.pause(self.reactor.monotonic()
+ HTU21D_DEVICES[self.deviceId][self.resolution][1])
params = self.i2c.i2c_read([],3)
response = bytearray(params['response'])
rhumid = response[0] << 8
rhumid|= response[1]
if self._chekCRC8(rhumid) != response[2]:
logging.warn("htu21d: Checksum error on Humidity reading!")
else:
#clear status bits,
# humidity always returns xxxxxx10 in the LSB field
rhumid ^= 0x02;
self.humidity = (0.001907 * float(rhumid) - 6)
if (self.humidity < 0):
#due to RH accuracy, measured value might be
# slightly less than 0 or more 100
self.humidity = 0
elif (self.humidity > 100):
self.humidity = 100
# Only for HTU21D & SHT21.
# Calculates temperature compensated Humidity, %RH
if( self.deviceId in ['SHT21','HTU21D']
and self.temp > 0 and self.temp < 80):
logging.debug("htu21d: Do temp compensation..")
self.humidity = self.humidity
+ (25.0 - self.temp) * HTU21D_TEMP_COEFFICIENT;
logging.debug("htu21d: Humidity %.2f " % self.humidity)
except Exception:
logging.exception("htu21d: Error reading data")
self.temp = self.humidity = .0
return self.reactor.NEVER
if self.temp < self.min_temp or self.temp > self.max_temp:
self.printer.invoke_shutdown(
"HTU21D temperature %0.1f outside range of %0.1f:%.01f"
% (self.temp, self.min_temp, self.max_temp))
measured_time = self.reactor.monotonic()
print_time = self.i2c.get_mcu().estimated_print_time(measured_time)
self._callback(print_time, self.temp)
return measured_time + self.report_time
def _chekCRC8(self,data):
for bit in range(0,16):
if (data & 0x8000):
data = (data << 1) ^ HTU21D_CRC8_POLYNOMINAL;
else:
data <<= 1
data = data >> 8
return data
def get_status(self, eventtime):
return {
'temperature': round(self.temp, 2),
'humidity': self.humidity,
}
def load_config(config):
# Register sensor
pheater = config.get_printer().lookup_object("heaters")
for stype in HTU21D_DEVICES:
pheater.add_sensor_factory(stype, HTU21D)

BIN
klippy/extras/htu21d.pyc Normal file

Binary file not shown.

View File

@@ -0,0 +1,116 @@
# Support for disabling the printer on an idle timeout
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
DEFAULT_IDLE_GCODE = """
{% if 'heaters' in printer %}
TURN_OFF_HEATERS
{% endif %}
M84
"""
PIN_MIN_TIME = 0.100
READY_TIMEOUT = .500
class IdleTimeout:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.gcode = self.printer.lookup_object('gcode')
self.toolhead = self.timeout_timer = None
self.printer.register_event_handler("klippy:ready", self.handle_ready)
self.idle_timeout = config.getfloat('timeout', 600., above=0.)
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.idle_gcode = gcode_macro.load_template(config, 'gcode',
DEFAULT_IDLE_GCODE)
self.gcode.register_command('SET_IDLE_TIMEOUT',
self.cmd_SET_IDLE_TIMEOUT,
desc=self.cmd_SET_IDLE_TIMEOUT_help)
self.state = "Idle"
self.last_print_start_systime = 0.
def get_status(self, eventtime):
printing_time = 0.
if self.state == "Printing":
printing_time = eventtime - self.last_print_start_systime
return { "state": self.state, "printing_time": printing_time }
def handle_ready(self):
self.toolhead = self.printer.lookup_object('toolhead')
self.timeout_timer = self.reactor.register_timer(self.timeout_handler)
self.printer.register_event_handler("toolhead:sync_print_time",
self.handle_sync_print_time)
def transition_idle_state(self, eventtime):
self.state = "Printing"
try:
script = self.idle_gcode.render()
res = self.gcode.run_script(script)
except:
logging.exception("idle timeout gcode execution")
self.state = "Ready"
return eventtime + 1.
print_time = self.toolhead.get_last_move_time()
self.state = "Idle"
self.printer.send_event("idle_timeout:idle", print_time)
return self.reactor.NEVER
def check_idle_timeout(self, eventtime):
# Make sure toolhead class isn't busy
print_time, est_print_time, lookahead_empty = self.toolhead.check_busy(
eventtime)
idle_time = est_print_time - print_time
if not lookahead_empty or idle_time < 1.:
# Toolhead is busy
return eventtime + self.idle_timeout
if idle_time < self.idle_timeout:
# Wait for idle timeout
return eventtime + self.idle_timeout - idle_time
if self.gcode.get_mutex().test():
# Gcode class busy
return eventtime + 1.
# Idle timeout has elapsed
return self.transition_idle_state(eventtime)
def timeout_handler(self, eventtime):
if self.printer.is_shutdown():
return self.reactor.NEVER
if self.state == "Ready":
return self.check_idle_timeout(eventtime)
# Check if need to transition to "ready" state
print_time, est_print_time, lookahead_empty = self.toolhead.check_busy(
eventtime)
buffer_time = min(2., print_time - est_print_time)
if not lookahead_empty:
# Toolhead is busy
return eventtime + READY_TIMEOUT + max(0., buffer_time)
if buffer_time > -READY_TIMEOUT:
# Wait for ready timeout
return eventtime + READY_TIMEOUT + buffer_time
if self.gcode.get_mutex().test():
# Gcode class busy
return eventtime + READY_TIMEOUT
# Transition to "ready" state
self.state = "Ready"
self.printer.send_event("idle_timeout:ready",
est_print_time + PIN_MIN_TIME)
return eventtime + self.idle_timeout
def handle_sync_print_time(self, curtime, print_time, est_print_time):
if self.state == "Printing":
return
# Transition to "printing" state
self.state = "Printing"
self.last_print_start_systime = curtime
check_time = READY_TIMEOUT + print_time - est_print_time
self.reactor.update_timer(self.timeout_timer, curtime + check_time)
self.printer.send_event("idle_timeout:printing",
est_print_time + PIN_MIN_TIME)
cmd_SET_IDLE_TIMEOUT_help = "Set the idle timeout in seconds"
def cmd_SET_IDLE_TIMEOUT(self, gcmd):
timeout = gcmd.get_float('TIMEOUT', self.idle_timeout, above=0.)
self.idle_timeout = timeout
gcmd.respond_info("idle_timeout: Timeout set to %.2f s" % (timeout,))
if self.state == "Ready":
checktime = self.reactor.monotonic() + timeout
self.reactor.update_timer(self.timeout_timer, checktime)
def load_config(config):
return IdleTimeout(config)

Binary file not shown.

View File

@@ -0,0 +1,163 @@
# Kinematic input shaper to minimize motion vibrations in XY plane
#
# Copyright (C) 2019-2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import collections
import chelper
from . import shaper_defs
class InputShaperParams:
def __init__(self, axis, config):
self.axis = axis
self.shapers = {s.name : s.init_func for s in shaper_defs.INPUT_SHAPERS}
shaper_type = config.get('shaper_type', 'mzv')
self.shaper_type = config.get('shaper_type_' + axis, shaper_type)
if self.shaper_type not in self.shapers:
raise config.error(
'Unsupported shaper type: %s' % (self.shaper_type,))
self.damping_ratio = config.getfloat('damping_ratio_' + axis,
shaper_defs.DEFAULT_DAMPING_RATIO,
minval=0., maxval=1.)
self.shaper_freq = config.getfloat('shaper_freq_' + axis, 0., minval=0.)
def update(self, gcmd):
axis = self.axis.upper()
self.damping_ratio = gcmd.get_float('DAMPING_RATIO_' + axis,
self.damping_ratio,
minval=0., maxval=1.)
self.shaper_freq = gcmd.get_float('SHAPER_FREQ_' + axis,
self.shaper_freq, minval=0.)
shaper_type = gcmd.get('SHAPER_TYPE', None)
if shaper_type is None:
shaper_type = gcmd.get('SHAPER_TYPE_' + axis, self.shaper_type)
if shaper_type.lower() not in self.shapers:
raise gcmd.error('Unsupported shaper type: %s' % (shaper_type,))
self.shaper_type = shaper_type.lower()
def get_shaper(self):
if not self.shaper_freq:
A, T = shaper_defs.get_none_shaper()
else:
A, T = self.shapers[self.shaper_type](
self.shaper_freq, self.damping_ratio)
return len(A), A, T
def get_status(self):
return collections.OrderedDict([
('shaper_type', self.shaper_type),
('shaper_freq', '%.3f' % (self.shaper_freq,)),
('damping_ratio', '%.6f' % (self.damping_ratio,))])
class AxisInputShaper:
def __init__(self, axis, config):
self.axis = axis
self.params = InputShaperParams(axis, config)
self.n, self.A, self.T = self.params.get_shaper()
self.saved = None
def get_name(self):
return 'shaper_' + self.axis
def get_shaper(self):
return self.n, self.A, self.T
def update(self, gcmd):
self.params.update(gcmd)
old_n, old_A, old_T = self.n, self.A, self.T
self.n, self.A, self.T = self.params.get_shaper()
return (old_n, old_A, old_T) != (self.n, self.A, self.T)
def set_shaper_kinematics(self, sk):
ffi_main, ffi_lib = chelper.get_ffi()
success = ffi_lib.input_shaper_set_shaper_params(
sk, self.axis.encode(), self.n, self.A, self.T) == 0
if not success:
self.disable_shaping()
ffi_lib.input_shaper_set_shaper_params(
sk, self.axis.encode(), self.n, self.A, self.T)
return success
def get_step_generation_window(self):
ffi_main, ffi_lib = chelper.get_ffi()
return ffi_lib.input_shaper_get_step_generation_window(self.n,
self.A, self.T)
def disable_shaping(self):
if self.saved is None and self.n:
self.saved = (self.n, self.A, self.T)
A, T = shaper_defs.get_none_shaper()
self.n, self.A, self.T = len(A), A, T
def enable_shaping(self):
if self.saved is None:
# Input shaper was not disabled
return
self.n, self.A, self.T = self.saved
self.saved = None
def report(self, gcmd):
info = ' '.join(["%s_%s:%s" % (key, self.axis, value)
for (key, value) in self.params.get_status().items()])
gcmd.respond_info(info)
class InputShaper:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect", self.connect)
self.toolhead = None
self.shapers = [AxisInputShaper('x', config),
AxisInputShaper('y', config)]
self.stepper_kinematics = []
self.orig_stepper_kinematics = []
# Register gcode commands
gcode = self.printer.lookup_object('gcode')
gcode.register_command("SET_INPUT_SHAPER",
self.cmd_SET_INPUT_SHAPER,
desc=self.cmd_SET_INPUT_SHAPER_help)
def get_shapers(self):
return self.shapers
def connect(self):
self.toolhead = self.printer.lookup_object("toolhead")
kin = self.toolhead.get_kinematics()
# Lookup stepper kinematics
ffi_main, ffi_lib = chelper.get_ffi()
steppers = kin.get_steppers()
for s in steppers:
sk = ffi_main.gc(ffi_lib.input_shaper_alloc(), ffi_lib.free)
orig_sk = s.set_stepper_kinematics(sk)
res = ffi_lib.input_shaper_set_sk(sk, orig_sk)
if res < 0:
s.set_stepper_kinematics(orig_sk)
continue
self.stepper_kinematics.append(sk)
self.orig_stepper_kinematics.append(orig_sk)
# Configure initial values
self.old_delay = 0.
self._update_input_shaping(error=self.printer.config_error)
def _update_input_shaping(self, error=None):
self.toolhead.flush_step_generation()
new_delay = max([s.get_step_generation_window() for s in self.shapers])
self.toolhead.note_step_generation_scan_time(new_delay,
old_delay=self.old_delay)
failed = []
for sk in self.stepper_kinematics:
for shaper in self.shapers:
if shaper in failed:
continue
if not shaper.set_shaper_kinematics(sk):
failed.append(shaper)
if failed:
error = error or self.printer.command_error
raise error("Failed to configure shaper(s) %s with given parameters"
% (', '.join([s.get_name() for s in failed])))
def disable_shaping(self):
for shaper in self.shapers:
shaper.disable_shaping()
self._update_input_shaping()
def enable_shaping(self):
for shaper in self.shapers:
shaper.enable_shaping()
self._update_input_shaping()
cmd_SET_INPUT_SHAPER_help = "Set cartesian parameters for input shaper"
def cmd_SET_INPUT_SHAPER(self, gcmd):
updated = False
for shaper in self.shapers:
updated |= shaper.update(gcmd)
if updated:
self._update_input_shaping()
for shaper in self.shapers:
shaper.report(gcmd)
def load_config(config):
return InputShaper(config)

Binary file not shown.

232
klippy/extras/led.py Normal file
View File

@@ -0,0 +1,232 @@
# Support for PWM driven LEDs
#
# Copyright (C) 2019-2022 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, ast
from .display import display
# Time between each led template update
RENDER_TIME = 0.500
# Helper code for common LED initialization and control
class LEDHelper:
def __init__(self, config, update_func, led_count=1):
self.printer = config.get_printer()
self.update_func = update_func
self.led_count = led_count
self.need_transmit = False
# Initial color
red = config.getfloat('initial_RED', 0., minval=0., maxval=1.)
green = config.getfloat('initial_GREEN', 0., minval=0., maxval=1.)
blue = config.getfloat('initial_BLUE', 0., minval=0., maxval=1.)
white = config.getfloat('initial_WHITE', 0., minval=0., maxval=1.)
self.led_state = [(red, green, blue, white)] * led_count
# Register commands
name = config.get_name().split()[-1]
gcode = self.printer.lookup_object('gcode')
gcode.register_mux_command("SET_LED", "LED", name, self.cmd_SET_LED,
desc=self.cmd_SET_LED_help)
def get_led_count(self):
return self.led_count
def set_color(self, index, color):
if index is None:
new_led_state = [color] * self.led_count
if self.led_state == new_led_state:
return
else:
if self.led_state[index - 1] == color:
return
new_led_state = list(self.led_state)
new_led_state[index - 1] = color
self.led_state = new_led_state
self.need_transmit = True
def check_transmit(self, print_time):
if not self.need_transmit:
return
self.need_transmit = False
try:
self.update_func(self.led_state, print_time)
except self.printer.command_error as e:
logging.exception("led update transmit error")
cmd_SET_LED_help = "Set the color of an LED"
def cmd_SET_LED(self, gcmd):
# Parse parameters
red = gcmd.get_float('RED', 0., minval=0., maxval=1.)
green = gcmd.get_float('GREEN', 0., minval=0., maxval=1.)
blue = gcmd.get_float('BLUE', 0., minval=0., maxval=1.)
white = gcmd.get_float('WHITE', 0., minval=0., maxval=1.)
index = gcmd.get_int('INDEX', None, minval=1, maxval=self.led_count)
transmit = gcmd.get_int('TRANSMIT', 1)
sync = gcmd.get_int('SYNC', 1)
color = (red, green, blue, white)
# Update and transmit data
def lookahead_bgfunc(print_time):
self.set_color(index, color)
if transmit:
self.check_transmit(print_time)
if sync:
#Sync LED Update with print time and send
toolhead = self.printer.lookup_object('toolhead')
toolhead.register_lookahead_callback(lookahead_bgfunc)
else:
#Send update now (so as not to wake toolhead and reset idle_timeout)
lookahead_bgfunc(None)
def get_status(self, eventtime=None):
return {'color_data': self.led_state}
# Main LED tracking code
class PrinterLED:
def __init__(self, config):
self.printer = config.get_printer()
self.led_helpers = {}
self.active_templates = {}
self.render_timer = None
# Load templates
dtemplates = display.lookup_display_templates(config)
self.templates = dtemplates.get_display_templates()
gcode_macro = self.printer.lookup_object("gcode_macro")
self.create_template_context = gcode_macro.create_template_context
# Register handlers
gcode = self.printer.lookup_object('gcode')
gcode.register_command("SET_LED_TEMPLATE", self.cmd_SET_LED_TEMPLATE,
desc=self.cmd_SET_LED_TEMPLATE_help)
def setup_helper(self, config, update_func, led_count=1):
led_helper = LEDHelper(config, update_func, led_count)
name = config.get_name().split()[-1]
self.led_helpers[name] = led_helper
return led_helper
def _activate_timer(self):
if self.render_timer is not None or not self.active_templates:
return
reactor = self.printer.get_reactor()
self.render_timer = reactor.register_timer(self._render, reactor.NOW)
def _activate_template(self, led_helper, index, template, lparams):
key = (led_helper, index)
if template is not None:
uid = (template,) + tuple(sorted(lparams.items()))
self.active_templates[key] = (uid, template, lparams)
return
if key in self.active_templates:
del self.active_templates[key]
def _render(self, eventtime):
if not self.active_templates:
# Nothing to do - unregister timer
reactor = self.printer.get_reactor()
reactor.register_timer(self.render_timer)
self.render_timer = None
return reactor.NEVER
# Setup gcode_macro template context
context = self.create_template_context(eventtime)
def render(name, **kwargs):
return self.templates[name].render(context, **kwargs)
context['render'] = render
# Render all templates
need_transmit = {}
rendered = {}
template_info = self.active_templates.items()
for (led_helper, index), (uid, template, lparams) in template_info:
color = rendered.get(uid)
if color is None:
try:
text = template.render(context, **lparams)
parts = [max(0., min(1., float(f)))
for f in text.split(',', 4)]
except Exception as e:
logging.exception("led template render error")
parts = []
if len(parts) < 4:
parts += [0.] * (4 - len(parts))
rendered[uid] = color = tuple(parts)
need_transmit[led_helper] = 1
led_helper.set_color(index, color)
context.clear() # Remove circular references for better gc
# Transmit pending changes
for led_helper in need_transmit.keys():
led_helper.check_transmit(None)
return eventtime + RENDER_TIME
cmd_SET_LED_TEMPLATE_help = "Assign a display_template to an LED"
def cmd_SET_LED_TEMPLATE(self, gcmd):
led_name = gcmd.get("LED")
led_helper = self.led_helpers.get(led_name)
if led_helper is None:
raise gcmd.error("Unknown LED '%s'" % (led_name,))
led_count = led_helper.get_led_count()
index = gcmd.get_int("INDEX", None, minval=1, maxval=led_count)
template = None
lparams = {}
tpl_name = gcmd.get("TEMPLATE")
if tpl_name:
template = self.templates.get(tpl_name)
if template is None:
raise gcmd.error("Unknown display_template '%s'" % (tpl_name,))
tparams = template.get_params()
for p, v in gcmd.get_command_parameters().items():
if not p.startswith("PARAM_"):
continue
p = p.lower()
if p not in tparams:
raise gcmd.error("Invalid display_template parameter: %s"
% (p,))
try:
lparams[p] = ast.literal_eval(v)
except ValueError as e:
raise gcmd.error("Unable to parse '%s' as a literal" % (v,))
if index is not None:
self._activate_template(led_helper, index, template, lparams)
else:
for i in range(led_count):
self._activate_template(led_helper, i+1, template, lparams)
self._activate_timer()
PIN_MIN_TIME = 0.100
MAX_SCHEDULE_TIME = 5.0
# Handler for PWM controlled LEDs
class PrinterPWMLED:
def __init__(self, config):
self.printer = printer = config.get_printer()
# Configure pwm pins
ppins = printer.lookup_object('pins')
cycle_time = config.getfloat('cycle_time', 0.010, above=0.,
maxval=MAX_SCHEDULE_TIME)
hardware_pwm = config.getboolean('hardware_pwm', False)
self.pins = []
for i, name in enumerate(("red", "green", "blue", "white")):
pin_name = config.get(name + '_pin', None)
if pin_name is None:
continue
mcu_pin = ppins.setup_pin('pwm', pin_name)
mcu_pin.setup_max_duration(0.)
mcu_pin.setup_cycle_time(cycle_time, hardware_pwm)
self.pins.append((i, mcu_pin))
if not self.pins:
raise config.error("No LED pin definitions found in '%s'"
% (config.get_name(),))
self.last_print_time = 0.
# Initialize color data
pled = printer.load_object(config, "led")
self.led_helper = pled.setup_helper(config, self.update_leds, 1)
self.prev_color = color = self.led_helper.get_status()['color_data'][0]
for idx, mcu_pin in self.pins:
mcu_pin.setup_start_value(color[idx], 0.)
def update_leds(self, led_state, print_time):
if print_time is None:
eventtime = self.printer.get_reactor().monotonic()
mcu = self.pins[0][1].get_mcu()
print_time = mcu.estimated_print_time(eventtime) + PIN_MIN_TIME
print_time = max(print_time, self.last_print_time + PIN_MIN_TIME)
color = led_state[0]
for idx, mcu_pin in self.pins:
if self.prev_color[idx] != color[idx]:
mcu_pin.set_pwm(print_time, color[idx])
self.last_print_time = print_time
self.prev_color = color
def get_status(self, eventtime=None):
return self.led_helper.get_status(eventtime)
def load_config(config):
return PrinterLED(config)
def load_config_prefix(config):
return PrinterPWMLED(config)

BIN
klippy/extras/led.pyc Normal file

Binary file not shown.

108
klippy/extras/lm75.py Normal file
View File

@@ -0,0 +1,108 @@
# Support for I2C based LM75/LM75A temperature sensors
#
# Copyright (C) 2020 Boleslaw Ciesielski <combolek@users.noreply.github.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import bus
LM75_CHIP_ADDR = 0x48
LM75_I2C_SPEED = 100000
LM75_REGS = {
'TEMP' : 0x00,
'CONF' : 0x01,
'THYST' : 0x02,
'TOS' : 0x03,
'PRODID' : 0x07 # TI LM75A chips only?
}
LM75_REPORT_TIME = .8
# Temperature can be sampled at any time but the read aborts
# the current conversion. Conversion time is 300ms so make
# sure not to read too often.
LM75_MIN_REPORT_TIME = .5
class LM75:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.reactor = self.printer.get_reactor()
self.i2c = bus.MCU_I2C_from_config(config, LM75_CHIP_ADDR,
LM75_I2C_SPEED)
self.mcu = self.i2c.get_mcu()
self.report_time = config.getfloat('lm75_report_time', LM75_REPORT_TIME,
minval=LM75_MIN_REPORT_TIME)
self.temp = self.min_temp = self.max_temp = 0.0
self.sample_timer = self.reactor.register_timer(self._sample_lm75)
self.printer.add_object("lm75 " + self.name, self)
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
def handle_connect(self):
self._init_lm75()
self.reactor.update_timer(self.sample_timer, self.reactor.NOW)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
def setup_callback(self, cb):
self._callback = cb
def get_report_time_delta(self):
return self.report_time
def degrees_from_sample(self, x):
# The temp sample is encoded in the top 9 bits of a 16-bit
# value. Resolution is 0.5 degrees C.
return x[0] + (x[1] >> 7) * 0.5
def _init_lm75(self):
# Check and report the chip ID but ignore errors since many
# chips don't have it
try:
prodid = self.read_register('PRODID', 1)[0]
logging.info("lm75: Chip ID %#x" % prodid)
except:
pass
def _sample_lm75(self, eventtime):
try:
sample = self.read_register('TEMP', 2)
self.temp = self.degrees_from_sample(sample)
except Exception:
logging.exception("lm75: Error reading data")
self.temp = 0.0
return self.reactor.NEVER
if self.temp < self.min_temp or self.temp > self.max_temp:
self.printer.invoke_shutdown(
"LM75 temperature %0.1f outside range of %0.1f:%.01f"
% (self.temp, self.min_temp, self.max_temp))
measured_time = self.reactor.monotonic()
self._callback(self.mcu.estimated_print_time(measured_time), self.temp)
return measured_time + self.report_time
def read_register(self, reg_name, read_len):
# read a single register
regs = [LM75_REGS[reg_name]]
params = self.i2c.i2c_read(regs, read_len)
return bytearray(params['response'])
def write_register(self, reg_name, data):
if type(data) is not list:
data = [data]
reg = LM75_REGS[reg_name]
data.insert(0, reg)
self.i2c.i2c_write(data)
def get_status(self, eventtime):
return {
'temperature': round(self.temp, 2),
}
def load_config(config):
# Register sensor
pheaters = config.get_printer().load_object(config, "heaters")
pheaters.add_sensor_factory("LM75", LM75)

BIN
klippy/extras/lm75.pyc Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More