mirror of
https://github.com/QIDITECH/klipper.git
synced 2026-01-30 23:48:43 +03:00
klipper update
This commit is contained in:
5
klippy/extras/__init__.py
Normal file
5
klippy/extras/__init__.py
Normal 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
BIN
klippy/extras/__init__.pyc
Normal file
Binary file not shown.
22
klippy/extras/ad5206.py
Normal file
22
klippy/extras/ad5206.py
Normal 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)
|
||||
79
klippy/extras/adc_scaled.py
Normal file
79
klippy/extras/adc_scaled.py
Normal 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)
|
||||
302
klippy/extras/adc_temperature.py
Normal file
302
klippy/extras/adc_temperature.py
Normal 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)
|
||||
BIN
klippy/extras/adc_temperature.pyc
Normal file
BIN
klippy/extras/adc_temperature.pyc
Normal file
Binary file not shown.
444
klippy/extras/adxl345.py
Normal file
444
klippy/extras/adxl345.py
Normal 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
BIN
klippy/extras/adxl345.pyc
Normal file
Binary file not shown.
578
klippy/extras/angle.py
Normal file
578
klippy/extras/angle.py
Normal 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
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
BIN
klippy/extras/bed_mesh.pyc
Normal file
Binary file not shown.
109
klippy/extras/bed_screws.py
Normal file
109
klippy/extras/bed_screws.py
Normal 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
99
klippy/extras/bed_tilt.py
Normal 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
276
klippy/extras/bltouch.py
Normal 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
BIN
klippy/extras/bltouch.pyc
Normal file
Binary file not shown.
480
klippy/extras/bme280.py
Normal file
480
klippy/extras/bme280.py
Normal 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
BIN
klippy/extras/bme280.pyc
Normal file
Binary file not shown.
27
klippy/extras/board_pins.py
Normal file
27
klippy/extras/board_pins.py
Normal 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)
|
||||
BIN
klippy/extras/board_pins.pyc
Normal file
BIN
klippy/extras/board_pins.pyc
Normal file
Binary file not shown.
252
klippy/extras/bus.py
Normal file
252
klippy/extras/bus.py
Normal 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
BIN
klippy/extras/bus.pyc
Normal file
Binary file not shown.
306
klippy/extras/buttons.py
Normal file
306
klippy/extras/buttons.py
Normal 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
BIN
klippy/extras/buttons.pyc
Normal file
Binary file not shown.
24
klippy/extras/canbus_ids.py
Normal file
24
klippy/extras/canbus_ids.py
Normal 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)
|
||||
71
klippy/extras/controller_fan.py
Normal file
71
klippy/extras/controller_fan.py
Normal 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)
|
||||
25
klippy/extras/dac084S085.py
Normal file
25
klippy/extras/dac084S085.py
Normal 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)
|
||||
54
klippy/extras/delayed_gcode.py
Normal file
54
klippy/extras/delayed_gcode.py
Normal 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)
|
||||
BIN
klippy/extras/delayed_gcode.pyc
Normal file
BIN
klippy/extras/delayed_gcode.pyc
Normal file
Binary file not shown.
287
klippy/extras/delta_calibrate.py
Normal file
287
klippy/extras/delta_calibrate.py
Normal 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)
|
||||
21
klippy/extras/display/__init__.py
Normal file
21
klippy/extras/display/__init__.py
Normal 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)
|
||||
BIN
klippy/extras/display/__init__.pyc
Normal file
BIN
klippy/extras/display/__init__.pyc
Normal file
Binary file not shown.
461
klippy/extras/display/display.cfg
Normal file
461
klippy/extras/display/display.cfg
Normal 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.
|
||||
271
klippy/extras/display/display.py
Normal file
271
klippy/extras/display/display.py
Normal 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)
|
||||
BIN
klippy/extras/display/display.pyc
Normal file
BIN
klippy/extras/display/display.pyc
Normal file
Binary file not shown.
276
klippy/extras/display/font8x14.py
Normal file
276
klippy/extras/display/font8x14.py
Normal 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'
|
||||
]
|
||||
BIN
klippy/extras/display/font8x14.pyc
Normal file
BIN
klippy/extras/display/font8x14.pyc
Normal file
Binary file not shown.
134
klippy/extras/display/hd44780.py
Normal file
134
klippy/extras/display/hd44780.py
Normal 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)
|
||||
BIN
klippy/extras/display/hd44780.pyc
Normal file
BIN
klippy/extras/display/hd44780.pyc
Normal file
Binary file not shown.
125
klippy/extras/display/hd44780_spi.py
Normal file
125
klippy/extras/display/hd44780_spi.py
Normal 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)
|
||||
BIN
klippy/extras/display/hd44780_spi.pyc
Normal file
BIN
klippy/extras/display/hd44780_spi.pyc
Normal file
Binary file not shown.
761
klippy/extras/display/menu.cfg
Normal file
761
klippy/extras/display/menu.cfg
Normal 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 %}
|
||||
1067
klippy/extras/display/menu.py
Normal file
1067
klippy/extras/display/menu.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
klippy/extras/display/menu.pyc
Normal file
BIN
klippy/extras/display/menu.pyc
Normal file
Binary file not shown.
108
klippy/extras/display/menu_keys.py
Normal file
108
klippy/extras/display/menu_keys.py
Normal 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!")
|
||||
BIN
klippy/extras/display/menu_keys.pyc
Normal file
BIN
klippy/extras/display/menu_keys.pyc
Normal file
Binary file not shown.
257
klippy/extras/display/st7920.py
Normal file
257
klippy/extras/display/st7920.py
Normal 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))
|
||||
BIN
klippy/extras/display/st7920.pyc
Normal file
BIN
klippy/extras/display/st7920.pyc
Normal file
Binary file not shown.
240
klippy/extras/display/uc1701.py
Normal file
240
klippy/extras/display/uc1701.py
Normal 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)
|
||||
BIN
klippy/extras/display/uc1701.pyc
Normal file
BIN
klippy/extras/display/uc1701.pyc
Normal file
Binary file not shown.
50
klippy/extras/display_status.py
Normal file
50
klippy/extras/display_status.py
Normal 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)
|
||||
BIN
klippy/extras/display_status.pyc
Normal file
BIN
klippy/extras/display_status.pyc
Normal file
Binary file not shown.
58
klippy/extras/dotstar.py
Normal file
58
klippy/extras/dotstar.py
Normal 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
80
klippy/extras/ds18b20.py
Normal 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
BIN
klippy/extras/ds18b20.pyc
Normal file
Binary file not shown.
15
klippy/extras/duplicate_pin_override.py
Normal file
15
klippy/extras/duplicate_pin_override.py
Normal 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)
|
||||
231
klippy/extras/endstop_phase.py
Normal file
231
klippy/extras/endstop_phase.py
Normal 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)
|
||||
302
klippy/extras/exclude_object.py
Normal file
302
klippy/extras/exclude_object.py
Normal 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)
|
||||
BIN
klippy/extras/exclude_object.pyc
Normal file
BIN
klippy/extras/exclude_object.pyc
Normal file
Binary file not shown.
20
klippy/extras/extruder_stepper.py
Normal file
20
klippy/extras/extruder_stepper.py
Normal 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
110
klippy/extras/fan.py
Normal 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
BIN
klippy/extras/fan.pyc
Normal file
Binary file not shown.
28
klippy/extras/fan_generic.py
Normal file
28
klippy/extras/fan_generic.py
Normal 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)
|
||||
BIN
klippy/extras/fan_generic.pyc
Normal file
BIN
klippy/extras/fan_generic.pyc
Normal file
Binary file not shown.
77
klippy/extras/filament_motion_sensor.py
Normal file
77
klippy/extras/filament_motion_sensor.py
Normal 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)
|
||||
118
klippy/extras/filament_switch_sensor.py
Normal file
118
klippy/extras/filament_switch_sensor.py
Normal 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)
|
||||
BIN
klippy/extras/filament_switch_sensor.pyc
Normal file
BIN
klippy/extras/filament_switch_sensor.pyc
Normal file
Binary file not shown.
74
klippy/extras/firmware_retraction.py
Normal file
74
klippy/extras/firmware_retraction.py
Normal 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
137
klippy/extras/force_move.py
Normal 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)
|
||||
BIN
klippy/extras/force_move.pyc
Normal file
BIN
klippy/extras/force_move.pyc
Normal file
Binary file not shown.
134
klippy/extras/gcode_arcs.py
Normal file
134
klippy/extras/gcode_arcs.py
Normal 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)
|
||||
BIN
klippy/extras/gcode_arcs.pyc
Normal file
BIN
klippy/extras/gcode_arcs.pyc
Normal file
Binary file not shown.
51
klippy/extras/gcode_button.py
Normal file
51
klippy/extras/gcode_button.py
Normal 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)
|
||||
191
klippy/extras/gcode_macro.py
Normal file
191
klippy/extras/gcode_macro.py
Normal 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)
|
||||
BIN
klippy/extras/gcode_macro.pyc
Normal file
BIN
klippy/extras/gcode_macro.pyc
Normal file
Binary file not shown.
276
klippy/extras/gcode_move.py
Normal file
276
klippy/extras/gcode_move.py
Normal 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)
|
||||
BIN
klippy/extras/gcode_move.pyc
Normal file
BIN
klippy/extras/gcode_move.pyc
Normal file
Binary file not shown.
224
klippy/extras/hall_filament_width_sensor.py
Normal file
224
klippy/extras/hall_filament_width_sensor.py
Normal 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)
|
||||
28
klippy/extras/heater_bed.py
Normal file
28
klippy/extras/heater_bed.py
Normal 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)
|
||||
BIN
klippy/extras/heater_bed.pyc
Normal file
BIN
klippy/extras/heater_bed.pyc
Normal file
Binary file not shown.
42
klippy/extras/heater_fan.py
Normal file
42
klippy/extras/heater_fan.py
Normal 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)
|
||||
BIN
klippy/extras/heater_fan.pyc
Normal file
BIN
klippy/extras/heater_fan.pyc
Normal file
Binary file not shown.
9
klippy/extras/heater_generic.py
Normal file
9
klippy/extras/heater_generic.py
Normal 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)
|
||||
BIN
klippy/extras/heater_generic.pyc
Normal file
BIN
klippy/extras/heater_generic.pyc
Normal file
Binary file not shown.
371
klippy/extras/heaters.py
Normal file
371
klippy/extras/heaters.py
Normal 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
BIN
klippy/extras/heaters.pyc
Normal file
Binary file not shown.
277
klippy/extras/homing.py
Normal file
277
klippy/extras/homing.py
Normal 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
BIN
klippy/extras/homing.pyc
Normal file
Binary file not shown.
64
klippy/extras/homing_heaters.py
Normal file
64
klippy/extras/homing_heaters.py
Normal 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)
|
||||
65
klippy/extras/homing_override.py
Normal file
65
klippy/extras/homing_override.py
Normal 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)
|
||||
BIN
klippy/extras/homing_override.pyc
Normal file
BIN
klippy/extras/homing_override.pyc
Normal file
Binary file not shown.
248
klippy/extras/htu21d.py
Normal file
248
klippy/extras/htu21d.py
Normal 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
BIN
klippy/extras/htu21d.pyc
Normal file
Binary file not shown.
116
klippy/extras/idle_timeout.py
Normal file
116
klippy/extras/idle_timeout.py
Normal 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)
|
||||
BIN
klippy/extras/idle_timeout.pyc
Normal file
BIN
klippy/extras/idle_timeout.pyc
Normal file
Binary file not shown.
163
klippy/extras/input_shaper.py
Normal file
163
klippy/extras/input_shaper.py
Normal 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)
|
||||
BIN
klippy/extras/input_shaper.pyc
Normal file
BIN
klippy/extras/input_shaper.pyc
Normal file
Binary file not shown.
232
klippy/extras/led.py
Normal file
232
klippy/extras/led.py
Normal 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
BIN
klippy/extras/led.pyc
Normal file
Binary file not shown.
108
klippy/extras/lm75.py
Normal file
108
klippy/extras/lm75.py
Normal 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
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
Reference in New Issue
Block a user