mirror of
https://github.com/Dark98/Ender-3_V3_KE_Klipper.git
synced 2026-10-01 02:18:08 +00:00
update for open source
This commit is contained in:
@@ -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.
|
||||
@@ -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("""{"code":"key188", "msg": "vref and vssa must be on same mcu", "values": []}""")
|
||||
# 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("""{"code":"key189", "msg": "adc_scaled only supports adc pins", "values": []}""")
|
||||
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)
|
||||
@@ -0,0 +1,315 @@
|
||||
# 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)
|
||||
self.temp_offset_flag = config.getboolean('temp_offset_flag', default=False)
|
||||
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):
|
||||
if self.temp_offset_flag == True:
|
||||
vpt = [40, 52, 84.4,118.4]
|
||||
temp1 = self.adc_convert.calc_temp(read_value)
|
||||
if (temp1 > vpt[0]) & (temp1 < vpt[1]):
|
||||
temp = (temp1 - vpt[0]) * (10/(vpt[1] - vpt[0])) + vpt[0]
|
||||
elif (temp1 >= vpt[1]) & (temp1 < vpt[2]):
|
||||
temp = (temp1 - vpt[1]) * (30/(vpt[2] - vpt[1])) + 50
|
||||
elif temp1 >= vpt[2]:
|
||||
temp = (temp1 - vpt[2]) * (30/(vpt[3] - vpt[2])) + 80
|
||||
elif temp1 < vpt[0]:
|
||||
temp = self.adc_convert.calc_temp(read_value)
|
||||
else:
|
||||
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("""{"code":"key26", "msg":"duplicate value", "values": []}""")
|
||||
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("""{"code":"key27", "msg":"need at least two samples", "values": []}""")
|
||||
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("""{"code":"key28", "msg":"adc_temperature %s in heater %s", "values": ["%s", "%s"]}""" % (
|
||||
str(e), config.get_name(), 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("""{"code":"key28", "msg":"adc_temperature %s in heater %s", "values": ["%s", "%s"]}""" % (
|
||||
str(e), config.get_name(), 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)
|
||||
Executable
+524
@@ -0,0 +1,524 @@
|
||||
# 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
|
||||
import struct
|
||||
from multiprocessing import shared_memory
|
||||
|
||||
# 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_XY = 0.003774 * FREEFALL_ACCEL # 1 / 265 (at 3.3V) mg/LSB
|
||||
SCALE_Z = 0.003906 * FREEFALL_ACCEL # 1 / 256 (at 3.3V) mg/LSB
|
||||
|
||||
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 copy_double_to_buffer(self, buffer, offset, val):
|
||||
bytes = struct.pack("d", val)
|
||||
|
||||
# little store
|
||||
try:
|
||||
buffer[offset:offset+8] = bytearray(bytes)
|
||||
del bytes
|
||||
except:
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.respond_info("val: %f, bytes: %s, offset: %d" % (val, bytes.hex(), offset))
|
||||
|
||||
def copy_int_to_buffer(self, buffer, offset, val):
|
||||
# little store
|
||||
try:
|
||||
buffer[offset] = val & 0xFF
|
||||
buffer[offset + 1] = (val >> 8) & 0xFF
|
||||
buffer[offset + 2] = (val >> 16) & 0xFF
|
||||
buffer[offset + 3] = (val >> 24) & 0xFF
|
||||
except:
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.respond_info("val: %f, offset: %d" % (val, offset))
|
||||
|
||||
def get_samples_to_shared_mem(self):
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
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
|
||||
|
||||
# shm size = (double bytes) * (count of member: samp_time, x, y and z) * total
|
||||
shm_size = 8 * 4 * total
|
||||
shm = shared_memory.SharedMemory(name="psm_samples", create=True, size=shm_size)
|
||||
|
||||
buffer = shm.buf
|
||||
self.copy_int_to_buffer(buffer, 0, count)
|
||||
count += 4
|
||||
|
||||
reactor = self.printer.get_reactor()
|
||||
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
|
||||
|
||||
# 30000 * sizeof(samp_time, x, y, z) + sizeof(count)
|
||||
# switch process
|
||||
if count % 960000 == 4:
|
||||
reactor.pause(reactor.monotonic() + .1)
|
||||
|
||||
self.copy_double_to_buffer(buffer, count, samp_time)
|
||||
count += 8
|
||||
self.copy_double_to_buffer(buffer, count, x)
|
||||
count += 8
|
||||
self.copy_double_to_buffer(buffer, count, y)
|
||||
count += 8
|
||||
self.copy_double_to_buffer(buffer, count, z)
|
||||
count += 8
|
||||
|
||||
self.copy_int_to_buffer(buffer, 0, count)
|
||||
shm.close()
|
||||
gcode.respond_info("shm_size: %d, double bytes count: %d" % (shm_size, count))
|
||||
|
||||
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)
|
||||
webhooks = self.printer.lookup_object('webhooks')
|
||||
webhooks.register_endpoint("getAdxl345Status",
|
||||
self.get_adxl345_status)
|
||||
def get_adxl345_status(self, web_request):
|
||||
adxl345_is_exist = True
|
||||
try:
|
||||
aclient = self.chip.start_internal_client()
|
||||
self.printer.lookup_object('toolhead').dwell(1.)
|
||||
aclient.finish_measurements()
|
||||
values = aclient.get_samples()
|
||||
except Exception as err:
|
||||
logging.error(err)
|
||||
values = ""
|
||||
if not values:
|
||||
adxl345_is_exist = False
|
||||
web_request.send({"adxl345_is_exist": adxl345_is_exist})
|
||||
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("""{"code":"key64", "msg":"Invalid adxl345 NAME parameter", "values": []}""")
|
||||
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("""{"code":"key232", "msg":"No adxl345 measurements found", "values": []}""")
|
||||
_, 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_XY), 'y': (1, SCALE_XY), 'z': (2, SCALE_Z),
|
||||
'-x': (0, -SCALE_XY), '-y': (1, -SCALE_XY), '-z': (2, -SCALE_Z)}
|
||||
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('{"code": "key9", "msg": "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("""{"code":"key245", "msg":"Invalid rate parameter: %d", "values": [%d]}""" % (self.data_rate,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(
|
||||
"""{"code":"key65", "msg":"Failed to set ADXL345 register [0x%x] to 0x%x: got 0x%x. \nThis is generally indicative of connection problems\n(e.g. faulty wiring)\nor a faulty adxl345 chip.", "values": ["%x","%x","%x"]}""" % (
|
||||
reg, val, stored_val, 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("""{"code":"key118", "msg":"Unable to query adxl345 fifo", "values": []}""")
|
||||
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(
|
||||
"""{"code":"key119", "msg": "Invalid adxl345 id (got %x vs %x).This is generally indicative of connection problems(e.g. faulty wiring) or a faulty adxl345 chip.", "values": ["%x", "%x"]}"""
|
||||
% (dev_id, ADXL345_DEV_ID, 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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
||||
# 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.reset()
|
||||
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 reset(self):
|
||||
self.state = None
|
||||
self.current_screw = 0
|
||||
self.accepted_screws = 0
|
||||
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)
|
||||
def get_status(self, eventtime):
|
||||
return {
|
||||
'is_active': self.state is not None,
|
||||
'state': self.state,
|
||||
'current_screw': self.current_screw,
|
||||
'accepted_screws': self.accepted_screws
|
||||
}
|
||||
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("""{"code":"key101", "msg": "Already in bed_screws helper; use ABORT to exit", "values": []}""")
|
||||
# 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.reset()
|
||||
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.reset()
|
||||
|
||||
def load_config(config):
|
||||
return BedScrews(config)
|
||||
@@ -0,0 +1,301 @@
|
||||
# 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
|
||||
import struct
|
||||
from . import bus
|
||||
|
||||
BL24C16F_CHIP_ADDR_0 = 0x50
|
||||
BL24C16F_CHIP_ADDR_1 = 0x51
|
||||
BL24C16F_CHIP_ADDR_2 = 0x52
|
||||
BL24C16F_CHIP_ADDR_3 = 0x53
|
||||
BL24C16F_CHIP_ADDR_4 = 0x54
|
||||
BL24C16F_CHIP_ADDR_5 = 0x55
|
||||
BL24C16F_CHIP_ADDR_6 = 0x56
|
||||
BL24C16F_CHIP_ADDR_7 = 0x57
|
||||
|
||||
class EEPROMCommandHelper:
|
||||
def __init__(self, config, chip):
|
||||
self.printer = config.get_printer()
|
||||
self.chip = chip
|
||||
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 == "bl24c16f" or not config.has_section("bl24c16f"):
|
||||
self.register_commands(None)
|
||||
|
||||
def register_commands(self, name):
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_mux_command("EEPROM_DEBUG_READ", "CHIP", name,
|
||||
self.cmd_EEPROM_DEBUG_READ,
|
||||
desc=self.cmd_EEPROM_DEBUG_READ_help)
|
||||
gcode.register_mux_command("EEPROM_DEBUG_WRITE_BYTE", "CHIP", name,
|
||||
self.cmd_EEPROM_DEBUG_WRITE_BYTE,
|
||||
desc=self.cmd_EEPROM_DEBUG_WRITE_BYTE_help)
|
||||
gcode.register_mux_command("EEPROM_DEBUG_WRITE_INT", "CHIP", name,
|
||||
self.cmd_EEPROM_DEBUG_WRITE_INT,
|
||||
desc=self.cmd_EEPROM_DEBUG_WRITE_INT_help)
|
||||
gcode.register_mux_command("EEPROM_DEBUG_WRITE_FLOAT", "CHIP", name,
|
||||
self.cmd_EEPROM_DEBUG_WRITE_FLOAT,
|
||||
desc=self.cmd_EEPROM_DEBUG_WRITE_FLOAT_help)
|
||||
|
||||
gcode.register_mux_command("EEPROM_READ", "CHIP", name,
|
||||
self.cmd_EEPROM_READ,
|
||||
desc=self.cmd_EEPROM_READ_help)
|
||||
gcode.register_mux_command("EEPROM_WRITE_BYTE", "CHIP", name,
|
||||
self.cmd_EEPROM_WRITE_BYTE,
|
||||
desc=self.cmd_EEPROM_WRITE_BYTE_help)
|
||||
gcode.register_mux_command("EEPROM_WRITE_INT", "CHIP", name,
|
||||
self.cmd_EEPROM_WRITE_INT,
|
||||
desc=self.cmd_EEPROM_WRITE_INT_help)
|
||||
gcode.register_mux_command("EEPROM_WRITE_FLOAT", "CHIP", name,
|
||||
self.cmd_EEPROM_WRITE_FLOAT,
|
||||
desc=self.cmd_EEPROM_WRITE_FLOAT_help)
|
||||
|
||||
gcode.register_mux_command("EEPROM_IS_FIRST_USED", "CHIP", name,
|
||||
self.cmd_EEPROM_IS_FIRST_USED)
|
||||
gcode.register_mux_command("EEPROM_POS", "CHIP", name,
|
||||
self.cmd_EEPROM_POS)
|
||||
gcode.register_mux_command("EEPROM_PRINTER_INFO", "CHIP", name,
|
||||
self.cmd_EEPROM_PRINTER_INFO)
|
||||
|
||||
def cmd_EEPROM_IS_FIRST_USED(self, gcmd):
|
||||
val = self.chip.read_reg(1, 1)
|
||||
state = False if int.from_bytes(val, 'little') != 255 else True
|
||||
gcmd.respond_info("EEPROM_IS_USED val:%s state:%s" % (int.from_bytes(val, 'little'), state))
|
||||
if int.from_bytes(val, 'little') != 255:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def cmd_EEPROM_POS(self, gcmd):
|
||||
pos = self.chip.read_reg(0, 1)
|
||||
gcmd.respond_info("EEPROM_POS int_pos:%s, pos:%s" % (int.from_bytes(pos, 'little'), pos))
|
||||
|
||||
def cmd_EEPROM_PRINTER_INFO(self, gcmd):
|
||||
pos = int.from_bytes(self.chip.read_reg(0, 1), 'little')
|
||||
file_position = self.chip.read_reg(pos*8, 4)
|
||||
base_position_e = self.chip.read_reg(pos*8+4, 4)
|
||||
ret = {"file_position": int.from_bytes(file_position, 'little'), "base_position_e": struct.unpack('f', base_position_e)[0]}
|
||||
gcmd.respond_info("EEPROM_PRINTER_INFO ret:%s" % str(ret))
|
||||
|
||||
def cmd_EEPROM_DEBUG_READ(self, gcmd):
|
||||
addr = gcmd.get("ADDR", minval=0, maxval=2047, parser=lambda x: int(x, 0))
|
||||
size = gcmd.get("SIZE", minval=0, maxval=56, parser=lambda x: int(x, 0))
|
||||
vals = self.chip.read_reg(addr, size)
|
||||
gcmd.respond_info("EEPROM_DEBUG_READ size: 0x%x" % size)
|
||||
reg_vals = 'read vals: '
|
||||
for i in range(size):
|
||||
if i % 16 == 0:
|
||||
reg_vals += '\n'
|
||||
reg_vals += '0x%x ' % vals[i]
|
||||
|
||||
gcmd.respond_info(reg_vals)
|
||||
|
||||
cmd_EEPROM_DEBUG_READ_help = "Read data bytes from eeprom"
|
||||
|
||||
def cmd_EEPROM_DEBUG_WRITE_BYTE(self, gcmd):
|
||||
addr = gcmd.get("ADDR", minval=0, maxval=2047, parser=lambda x: int(x, 0))
|
||||
val = gcmd.get("VAL", minval=0, maxval=255, parser=lambda x: int(x, 0))
|
||||
gcmd.respond_info("EEPROM_DEBUG_WRITE_BYTE : ADDR[0x%x] = 0x%x" % (addr, val))
|
||||
self.chip.write_reg(addr, val)
|
||||
|
||||
cmd_EEPROM_DEBUG_WRITE_BYTE_help = "Write byte data to eeprom"
|
||||
|
||||
def cmd_EEPROM_DEBUG_WRITE_INT(self, gcmd):
|
||||
pos = self.chip.read_reg(0, 1)
|
||||
gcmd.respond_info("EEPROM_POS int_pos:%s" % int.from_bytes(pos, 'little'))
|
||||
addr = gcmd.get("ADDR", minval=0, maxval=2047, parser=lambda x: int(x, 0))
|
||||
val = gcmd.get("VAL", minval=0, maxval=4294967296, parser=lambda x: int(x, 0))
|
||||
gcmd.respond_info("EEPROM_DEBUG_WRITE_INT : val = %d" % val)
|
||||
vals = [val & 0xFF]
|
||||
vals += [ (val >> 8) & 0xFF,
|
||||
(val >> 16) & 0xFF,
|
||||
(val >> 24) & 0xFF,
|
||||
]
|
||||
gcmd.respond_info("EEPROM_DEBUG_WRITE_INT : ADDR[0x%x] = 0x%02x 0x%02x 0x%02x 0x%02x"
|
||||
% (addr, vals[0], vals[1], vals[2], vals[3]))
|
||||
self.chip.write_reg(addr, vals)
|
||||
|
||||
cmd_EEPROM_DEBUG_WRITE_INT_help = "Write int (4 byte) data to eeprom"
|
||||
|
||||
def cmd_EEPROM_DEBUG_WRITE_FLOAT(self, gcmd):
|
||||
addr = gcmd.get("ADDR", minval=0, maxval=2047, parser=lambda x: int(x, 0))
|
||||
val = gcmd.get_float("VAL", 0.)
|
||||
gcmd.respond_info("EEPROM_DEBUG_WRITE_FLOAT : val = %f" % val)
|
||||
bs = struct.pack("f", val)
|
||||
data = int.from_bytes(bs, byteorder="little")
|
||||
|
||||
vals = [data & 0xFF]
|
||||
vals += [ (data >> 8) & 0xFF,
|
||||
(data >> 16) & 0xFF,
|
||||
(data >> 24) & 0xFF
|
||||
]
|
||||
gcmd.respond_info("EEPROM_DEBUG_WRITE_FLOAT : ADDR[0x%x] = 0x%02x 0x%02x 0x%02x 0x%02x"
|
||||
% (addr, vals[0], vals[1], vals[2], vals[3]))
|
||||
self.chip.write_reg(addr, vals)
|
||||
|
||||
cmd_EEPROM_DEBUG_WRITE_FLOAT_help = "Write float (4 byte) data to eeprom"
|
||||
|
||||
def cmd_EEPROM_READ(self, gcmd):
|
||||
addr = gcmd.get("ADDR", minval=0, maxval=2047, parser=lambda x: int(x, 0))
|
||||
size = gcmd.get("SIZE", minval=0, maxval=56, parser=lambda x: int(x, 0))
|
||||
vals = self.chip.read_reg(addr, size)
|
||||
# gcmd.respond_info("EEPROM_READ size: 0x%x" % size)
|
||||
reg_vals = 'read vals: '
|
||||
for i in range(size):
|
||||
if i % 16 == 0:
|
||||
reg_vals += '\n'
|
||||
reg_vals += '0x%x ' % vals[i]
|
||||
# gcmd.respond_info(reg_vals)
|
||||
cmd_EEPROM_READ_help = "Read data bytes from eeprom"
|
||||
|
||||
def cmd_EEPROM_WRITE_BYTE(self, gcmd):
|
||||
addr = gcmd.get("ADDR", minval=0, maxval=2047, parser=lambda x: int(x, 0))
|
||||
val = gcmd.get("VAL", minval=0, maxval=255, parser=lambda x: int(x, 0))
|
||||
# gcmd.respond_info("EEPROM_WRITE_BYTE : ADDR[0x%x] = 0x%x" % (addr, val))
|
||||
self.chip.write_reg(addr, val)
|
||||
cmd_EEPROM_WRITE_BYTE_help = "Write byte data to eeprom"
|
||||
|
||||
def cmd_EEPROM_WRITE_INT(self, gcmd):
|
||||
# pos = self.chip.read_reg(0, 1)
|
||||
# gcmd.respond_info("EEPROM_POS int_pos:%s" % int.from_bytes(pos, 'little'))
|
||||
addr = gcmd.get("ADDR", minval=0, maxval=2047, parser=lambda x: int(x, 0))
|
||||
val = gcmd.get("VAL", minval=0, maxval=4294967296, parser=lambda x: int(x, 0))
|
||||
# gcmd.respond_info("EEPROM_WRITE_INT : val = %d" % val)
|
||||
vals = [val & 0xFF]
|
||||
vals += [ (val >> 8) & 0xFF,
|
||||
(val >> 16) & 0xFF,
|
||||
(val >> 24) & 0xFF,
|
||||
]
|
||||
# gcmd.respond_info("EEPROM_WRITE_INT : ADDR[0x%x] = 0x%02x 0x%02x 0x%02x 0x%02x"
|
||||
# % (addr, vals[0], vals[1], vals[2], vals[3]))
|
||||
self.chip.write_reg(addr, vals)
|
||||
cmd_EEPROM_WRITE_INT_help = "Write int (4 byte) data to eeprom"
|
||||
|
||||
def cmd_EEPROM_WRITE_FLOAT(self, gcmd):
|
||||
addr = gcmd.get("ADDR", minval=0, maxval=2047, parser=lambda x: int(x, 0))
|
||||
val = gcmd.get_float("VAL", 0.)
|
||||
# gcmd.respond_info("EEPROM_WRITE_FLOAT : val = %f" % val)
|
||||
bs = struct.pack("f", val)
|
||||
data = int.from_bytes(bs, byteorder="little")
|
||||
|
||||
vals = [data & 0xFF]
|
||||
vals += [ (data >> 8) & 0xFF,
|
||||
(data >> 16) & 0xFF,
|
||||
(data >> 24) & 0xFF
|
||||
]
|
||||
# gcmd.respond_info("EEPROM_WRITE_FLOAT : ADDR[0x%x] = 0x%02x 0x%02x 0x%02x 0x%02x"
|
||||
# % (addr, vals[0], vals[1], vals[2], vals[3]))
|
||||
self.chip.write_reg(addr, vals)
|
||||
cmd_EEPROM_WRITE_FLOAT_help = "Write float (4 byte) data to eeprom"
|
||||
|
||||
class BL24C16F:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
EEPROMCommandHelper(config, self)
|
||||
self.name = config.get_name().split()[-1]
|
||||
self.reactor = self.printer.get_reactor()
|
||||
self.i2c0 = bus.MCU_I2C_from_config(
|
||||
config, default_addr=BL24C16F_CHIP_ADDR_0, default_speed=400000)
|
||||
self.i2c1 = bus.MCU_I2C_from_config(
|
||||
config, default_addr=BL24C16F_CHIP_ADDR_1, default_speed=400000)
|
||||
self.i2c2 = bus.MCU_I2C_from_config(
|
||||
config, default_addr=BL24C16F_CHIP_ADDR_2, default_speed=400000)
|
||||
self.i2c3 = bus.MCU_I2C_from_config(
|
||||
config, default_addr=BL24C16F_CHIP_ADDR_3, default_speed=400000)
|
||||
self.i2c4 = bus.MCU_I2C_from_config(
|
||||
config, default_addr=BL24C16F_CHIP_ADDR_4, default_speed=400000)
|
||||
self.i2c5 = bus.MCU_I2C_from_config(
|
||||
config, default_addr=BL24C16F_CHIP_ADDR_5, default_speed=400000)
|
||||
self.i2c6 = bus.MCU_I2C_from_config(
|
||||
config, default_addr=BL24C16F_CHIP_ADDR_6, default_speed=400000)
|
||||
self.i2c7 = bus.MCU_I2C_from_config(
|
||||
config, default_addr=BL24C16F_CHIP_ADDR_7, default_speed=400000)
|
||||
self.mcu = self.i2c0.get_mcu()
|
||||
self.printer.add_object("bl24c16f " + self.name, self)
|
||||
self.printer.register_event_handler("klippy:connect",
|
||||
self.handle_connect)
|
||||
|
||||
def handle_connect(self):
|
||||
self._init_bl24c16f()
|
||||
|
||||
def _init_bl24c16f(self):
|
||||
logging.info("bl24c16f init...")
|
||||
|
||||
def read_reg(self, addr, read_len):
|
||||
index = addr // 256
|
||||
offset = addr % 256
|
||||
reg = [offset]
|
||||
|
||||
if index == 0 :
|
||||
params = self.i2c0.i2c_read(reg, read_len)
|
||||
elif index == 1 :
|
||||
params = self.i2c1.i2c_read(reg, read_len)
|
||||
elif index == 2 :
|
||||
params = self.i2c2.i2c_read(reg, read_len)
|
||||
elif index == 3 :
|
||||
params = self.i2c3.i2c_read(reg, read_len)
|
||||
elif index == 4 :
|
||||
params = self.i2c4.i2c_read(reg, read_len)
|
||||
elif index == 5 :
|
||||
params = self.i2c5.i2c_read(reg, read_len)
|
||||
elif index == 6 :
|
||||
params = self.i2c6.i2c_read(reg, read_len)
|
||||
elif index == 7 :
|
||||
params = self.i2c7.i2c_read(reg, read_len)
|
||||
|
||||
return bytearray(params['response'])
|
||||
|
||||
def write_reg(self, addr, data):
|
||||
if type(data) is not list:
|
||||
data = [data]
|
||||
|
||||
index = addr // 256
|
||||
offset = addr % 256
|
||||
data.insert(0, offset)
|
||||
if index == 0 :
|
||||
self.i2c0.i2c_write(data)
|
||||
if index == 1 :
|
||||
self.i2c1.i2c_write(data)
|
||||
if index == 2 :
|
||||
self.i2c2.i2c_write(data)
|
||||
if index == 3 :
|
||||
self.i2c3.i2c_write(data)
|
||||
if index == 4 :
|
||||
self.i2c4.i2c_write(data)
|
||||
if index == 5 :
|
||||
self.i2c5.i2c_write(data)
|
||||
if index == 6 :
|
||||
self.i2c6.i2c_write(data)
|
||||
if index == 7 :
|
||||
self.i2c7.i2c_write(data)
|
||||
|
||||
def setEepromDisable(self):
|
||||
self.write_reg(1, 255)
|
||||
|
||||
def checkEepromFirstEnable(self):
|
||||
val = self.read_reg(1, 1)
|
||||
if int.from_bytes(val, 'little') != 255:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def eepromReadHeader(self):
|
||||
pos = self.read_reg(0, 1)
|
||||
return int.from_bytes(pos, 'little')
|
||||
|
||||
def eepromReadBody(self, pos):
|
||||
file_position = self.read_reg(pos*8, 4)
|
||||
base_position_e = self.read_reg(pos*8+4, 4)
|
||||
return {"file_position": int.from_bytes(file_position, 'little'), "base_position_e": struct.unpack('f', base_position_e)[0]}
|
||||
|
||||
def load_config(config):
|
||||
return BL24C16F(config)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return BL24C16F(config)
|
||||
@@ -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(
|
||||
'{"code": "key8", "msg": "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("""{"code":"key194", "msg": "BLTouch failed to deploy.", "values": []}""")
|
||||
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("""{"code":"key218", "msg": "BLTouch commands: %s.", "values": ["%s"]}""" % (
|
||||
", ".join(sorted([c for c in Commands if c is not None])), ", ".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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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("""{"code": "key310", "msg": "Must specify %s on mcu '%s'", "values":["%s", "%s"]}""" % (param, mcu_name, param, mcu_name))
|
||||
bus = rev_enums[0]
|
||||
if bus not in enums:
|
||||
raise ppins.error("""{"code": "key311", "msg": "Unknown %s '%s'", "values":["%s", "%s"]}""" % (param, bus, 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("""{"code":"key231", "msg":"%s spi pins must be on same mcu", "values": ["%s"]}""" % (
|
||||
config.get_name(), 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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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(
|
||||
"""{"code":"key66", "msg":"One or more of these steppers are unknown: %s (valid steppers are: %s)", "values": ["%s", "%s"]}"""
|
||||
% (self.stepper_names, ", ".join(all_steppers), 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)
|
||||
Executable
+125
@@ -0,0 +1,125 @@
|
||||
# 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 time
|
||||
|
||||
class CUSTOM_MACRO:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.pheaters = None
|
||||
self.heater_hot = None
|
||||
self.extruder_temp=None
|
||||
self.bed_temp=None
|
||||
self.prtouch = None
|
||||
self.gcode.register_command("CX_PRINT_LEVELING_CALIBRATION", self.cmd_CX_PRINT_LEVELING_CALIBRATION, desc=self.cmd_CX_PRINT_LEVELING_CALIBRATION_help)
|
||||
self.gcode.register_command("CX_CLEAN_CALIBRATION_FLAGS", self.cmd_CX_CLEAN_CALIBRATION_FLAGS, desc=self.cmd_CX_CLEAN_CALIBRATION_FLAGS_help)
|
||||
self.gcode.register_command("CX_PRINT_DRAW_ONE_LINE", self.cmd_CX_PRINT_DRAW_ONE_LINE, desc=self.cmd_CX_PRINT_DRAW_ONE_LINE_help)
|
||||
self.default_extruder_temp = config.getfloat("default_extruder_temp", default=240.0)
|
||||
self.default_bed_temp = config.getfloat("default_bed_temp", default=50.0)
|
||||
self.g28_ext_temp = config.getfloat("g28_ext_temp", default=140.0)
|
||||
self.nozzle_clear = config.getboolean('nozzle_clear', True)
|
||||
self.calibration = config.getint('calibration', default=0)
|
||||
self.temp_diff = config.getfloat('temp_diff', default=70)
|
||||
self.leveling_calibration = 0
|
||||
self.calibration_zoffset_flags = config.getint('calibration_zoffset_flags', default=0)
|
||||
pass
|
||||
|
||||
|
||||
def get_status(self, eventtime):
|
||||
return {
|
||||
'leveling_calibration': self.leveling_calibration,
|
||||
'default_extruder_temp': self.default_extruder_temp,
|
||||
'default_bed_temp': self.default_bed_temp,
|
||||
'g28_ext_temp': self.g28_ext_temp
|
||||
}
|
||||
|
||||
cmd_CX_PRINT_LEVELING_CALIBRATION_help = "Start Print function,three parameter:EXTRUDER_TEMP(180-300),BED_TEMP(30-100),CALIBRATION(0 or 1)"
|
||||
def cmd_CX_PRINT_LEVELING_CALIBRATION(self, gcmd):
|
||||
self.extruder_temp = gcmd.get_float('EXTRUDER_TEMP', default=self.default_extruder_temp, minval=180.0, maxval=320.0)
|
||||
if self.extruder_temp < 220.0:
|
||||
self.extruder_temp = 220.0
|
||||
self.g28_ext_temp = self.extruder_temp - self.temp_diff
|
||||
if self.g28_ext_temp > 200.0:
|
||||
self.g28_ext_temp = 200.0
|
||||
try:
|
||||
self.prtouch = self.printer.lookup_object('prtouch_v2')
|
||||
except:
|
||||
self.prtouch = self.printer.lookup_object('prtouch')
|
||||
gcmd.respond_info("self.prtouch = prtouch")
|
||||
# self.prtouch.change_hot_min_temp(self.g28_ext_temp)
|
||||
self.bed_temp = gcmd.get_float('BED_TEMP', default=self.default_bed_temp, minval=30.0, maxval=130.0)
|
||||
self.leveling_calibration = gcmd.get_int('LEVELING_CALIBRATION', default=1, minval=0, maxval=1)
|
||||
|
||||
self.gcode.run_script_from_command('G28')
|
||||
if (self.calibration_zoffset_flags == 0):
|
||||
self.gcode.run_script_from_command('M104 S%d' % (self.g28_ext_temp))
|
||||
self.gcode.run_script_from_command('M140 S%d' % (self.bed_temp))
|
||||
self.gcode.run_script_from_command('CRTENSE_NOZZLE_CLEAR HOT_START_TEMP=%d HOT_RUB_TEMP=%d BED_ADDTEMP=%d' % (self.g28_ext_temp, self.extruder_temp - 20, self.bed_temp))
|
||||
if self.leveling_calibration == 1:
|
||||
# self.gcode.run_script_from_command('CHECK_BED_MESH AUTO_G29=1')
|
||||
if (self.calibration_zoffset_flags == 0):
|
||||
self.gcode.run_script_from_command('Z_OFFSET_CALIBRATION')
|
||||
self.gcode.run_script_from_command('M104S0')
|
||||
self.gcode.run_script_from_command('M107')
|
||||
self.gcode.run_script_from_command('G28 Z')
|
||||
else:
|
||||
self.gcode.run_script_from_command('M104S0')
|
||||
self.gcode.run_script_from_command('M107')
|
||||
self.gcode.run_script_from_command('M190 S%d' % (self.bed_temp))
|
||||
self.gcode.run_script_from_command('BED_MESH_CALIBRATE')
|
||||
self.gcode.run_script_from_command('CXSAVE_CONFIG')
|
||||
|
||||
pass
|
||||
|
||||
cmd_CX_CLEAN_CALIBRATION_FLAGS_help = "Clean calibration flags"
|
||||
def cmd_CX_CLEAN_CALIBRATION_FLAGS(self, gcmd):
|
||||
self.leveling_calibration = 0
|
||||
pass
|
||||
|
||||
cmd_CX_PRINT_DRAW_ONE_LINE_help = "Draw one line before printing"
|
||||
def cmd_CX_PRINT_DRAW_ONE_LINE(self, gcmd):
|
||||
self.gcode.run_script_from_command('G92 E0')
|
||||
self.gcode.run_script_from_command('G1 X10 Y10 Z2 F6000')
|
||||
self.gcode.run_script_from_command('G1 Z0.2 F300')
|
||||
self.pheaters = self.printer.lookup_object('heaters')
|
||||
self.heater_hot = self.printer.lookup_object('extruder').heater
|
||||
self.gcode.respond_info("can_break_flag = %d" % (self.pheaters.can_break_flag))
|
||||
self.gcode.run_script_from_command('M104 S%d' % (self.extruder_temp))
|
||||
self.gcode.run_script_from_command('M140 S%d' % (self.bed_temp))
|
||||
self.pheaters.set_temperature(self.heater_hot, self.extruder_temp, True)
|
||||
self.gcode.respond_info("can_break_flag = %d" % (self.pheaters.can_break_flag))
|
||||
while self.pheaters.can_break_flag == 1:
|
||||
time.sleep(1)
|
||||
self.gcode.respond_info("can_break_flag = %d" % (self.pheaters.can_break_flag))
|
||||
if self.pheaters.can_break_flag == 3:
|
||||
self.pheaters.can_break_flag = 0
|
||||
self.gcode.respond_info("can_break_flag is 3")
|
||||
self.gcode.run_script_from_command('G21')
|
||||
self.gcode.run_script_from_command('G92 E0')
|
||||
self.gcode.run_script_from_command('G1 F2400 E-0.5')
|
||||
self.gcode.run_script_from_command('SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=5')
|
||||
self.gcode.run_script_from_command('M204 S12000')
|
||||
self.gcode.run_script_from_command('G21')
|
||||
self.gcode.run_script_from_command('SET_VELOCITY_LIMIT ACCEL_TO_DECEL=6000')
|
||||
self.gcode.run_script_from_command('SET_PRESSURE_ADVANCE ADVANCE=0.04')
|
||||
self.gcode.run_script_from_command('SET_PRESSURE_ADVANCE SMOOTH_TIME=0.04')
|
||||
self.gcode.run_script_from_command('M220 S100')
|
||||
self.gcode.run_script_from_command('M221 S100')
|
||||
self.gcode.run_script_from_command('G1 Z2.0 F1200')
|
||||
self.gcode.run_script_from_command('G1 X0.1 Y20 Z0.3 F6000.0')
|
||||
self.gcode.run_script_from_command('G1 X0.1 Y180.0 Z0.3 F3000.0 E15')
|
||||
self.gcode.run_script_from_command('G1 X0.4 Y180.0 Z0.3 F3000.0')
|
||||
self.gcode.run_script_from_command('G1 X0.4 Y20 Z0.3 F3000.0 E30')
|
||||
self.gcode.run_script_from_command('G92 E0')
|
||||
self.gcode.run_script_from_command('G1 Z2.0 F1200')
|
||||
self.gcode.run_script_from_command('G1 F12000')
|
||||
self.gcode.run_script_from_command('G21')
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return CUSTOM_MACRO(config)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,111 @@
|
||||
# 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 mcu
|
||||
import time
|
||||
|
||||
class DirZCtl:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.toolhead = None
|
||||
self.mcu = mcu.get_printer_mcu(self.printer, config.get('use_mcu'))
|
||||
self.oid = self.mcu.create_oid()
|
||||
self.steppers = []
|
||||
self.mcu.register_config_callback(self._build_config)
|
||||
self.mcu.register_response(self._handle_debug_dirzctl, "debug_dirzctl", self.oid)
|
||||
self.mcu.register_response(self._handle_result_dirzctl, "result_dirzctl", self.oid)
|
||||
self.printer.register_event_handler('klippy:mcu_identify', self._handle_mcu_identify)
|
||||
self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown)
|
||||
self.printer.register_event_handler("klippy:disconnect", self._handle_disconnect)
|
||||
self.gcode = self.printer.lookup_object("gcode")
|
||||
self.gcode.register_command('DIRZCTL', self.cmd_DIRZCTL, desc=self.cmd_DIRZCTL_help)
|
||||
self.all_params = []
|
||||
self.hx711s = None
|
||||
self.mcu_freq = 72000000
|
||||
self.step_base = config.getfloat('step_base', default=2, minval=1, maxval=6)
|
||||
self.last_send_heart = 0.
|
||||
self.is_shutdown = True
|
||||
self.is_timeout = True
|
||||
pass
|
||||
|
||||
def _handle_mcu_identify(self):
|
||||
self.hx711s = self.printer.lookup_object('hx711s')
|
||||
self.steppers = []
|
||||
self.toolhead = self.printer.lookup_object('toolhead')
|
||||
for stepper in self.toolhead.get_kinematics().get_steppers():
|
||||
if stepper.is_active_axis('z'):
|
||||
self.steppers.append(stepper)
|
||||
self.mcu_freq = self.mcu.get_constant_float('CLOCK_FREQ')
|
||||
|
||||
# self.send_heart_beat_cmd = self.mcu.lookup_query_command(
|
||||
# "heart_beat_dirzctl oid=%c",
|
||||
# "heart_beat_dirzctl_result oid=%c",
|
||||
# oid=self.oid, cq=None)
|
||||
|
||||
self.is_shutdown = False
|
||||
self.is_timeout = False
|
||||
pass
|
||||
|
||||
def _build_config(self):
|
||||
self.mcu.add_config_cmd("config_dirzctl oid=%d z_count=%d" % (self.oid, len(self.steppers)))
|
||||
for i in range(len(self.steppers)):
|
||||
dir_pin, step_pin, ivt_dir, ivt_step = self.steppers[i].get_pin_info()
|
||||
self.mcu.add_config_cmd("add_dirzctl oid=%d index=%d dir_pin=%s step_pin=%s dir_invert=%d step_invert=%d" % (self.oid, i, dir_pin, step_pin, ivt_dir, ivt_step))
|
||||
|
||||
# self.run_cmd = self.mcu.lookup_command("run_dirzctl oid=%c direct=%c step_us=%u step_cnt=%u is_ck_con=%c", cq=None)
|
||||
self.run_cmd = self.mcu.lookup_command("run_dirzctl oid=%c direct=%c step_us=%u step_cnt=%u", cq=None)
|
||||
pass
|
||||
|
||||
def _handle_shutdown(self):
|
||||
self.is_shutdown = True
|
||||
pass
|
||||
|
||||
def _handle_disconnect(self):
|
||||
self.is_timeout = True
|
||||
pass
|
||||
|
||||
def _handle_debug_dirzctl(self, params):
|
||||
self.printer.lookup_object('prtouch').pnt_msg(str(params))
|
||||
pass
|
||||
|
||||
def _handle_result_dirzctl(self, params):
|
||||
self.all_params.append(params)
|
||||
# self.printer.lookup_object('prtouch').pnt_msg(str(params))
|
||||
pass
|
||||
|
||||
def get_params(self):
|
||||
return self.all_params, (self.all_params[0]['tick'] if len(self.all_params) > 0 else 0)
|
||||
|
||||
def check_and_run(self, direct, step_us, step_cnt, wait_finish=True, is_ck_con=False):
|
||||
if self.is_shutdown or self.is_timeout:
|
||||
pass
|
||||
if step_cnt != 0:
|
||||
self.all_params = []
|
||||
# self.run_cmd.send([self.oid, direct, step_us, step_cnt, 1 if is_ck_con else 0])
|
||||
self.run_cmd.send([self.oid, direct, step_us, step_cnt])
|
||||
t_start = time.time()
|
||||
while not (self.is_shutdown or self.is_timeout) and wait_finish and ((time.time() - t_start) < (1.5 * 1000 * 1000 * step_us * step_cnt)) and len(self.all_params) != 2:
|
||||
self.hx711s.delay_s(0.05)
|
||||
pass
|
||||
|
||||
def send_heart_beat(self):
|
||||
#if time.time() - self.last_send_heart > 0.1:
|
||||
# self.send_heart_beat_cmd.send([self.oid])
|
||||
# self.last_send_heart = time.time()
|
||||
pass
|
||||
|
||||
cmd_DIRZCTL_help = "Test DIRZCTL."
|
||||
# DIRZCTL DIRECT=1 STEP_US=1500 STEP_CNT=100
|
||||
def cmd_DIRZCTL(self, gcmd):
|
||||
index = gcmd.get_int('INDEX', len(self.steppers), minval=0, maxval=len(self.steppers))
|
||||
direct = gcmd.get_int('DIRECT', 1, minval=0, maxval=1)
|
||||
step_us = gcmd.get_int('STEP_US', 1500, minval=4, maxval=100000)
|
||||
step_cnt = gcmd.get_int('STEP_CNT', 256, minval=0, maxval=10000)
|
||||
self.check_and_run(direct, step_us, step_cnt, False, False)
|
||||
pass
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return DirZCtl(config)
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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(
|
||||
"""{"code":"key192", "msg": "A primary [display] section must be defined in printer.cfg to use auxilary displays", "values": []}""")
|
||||
name = config.get_name().split()[-1]
|
||||
if name == "display":
|
||||
raise config.error(
|
||||
"""{"code":"key193", "msg": "Section name [display display] is not valid. Please choose a different postfix.", "values": []}""")
|
||||
return display.load_config(config)
|
||||
@@ -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.
|
||||
@@ -0,0 +1,275 @@
|
||||
# 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())
|
||||
"""{"code":"key168", "msg": "Option '%s' in section '%s' is not a valid literal", "values": ["%s", "%s"]}""" % (
|
||||
option, config.get_name(), 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(
|
||||
"""{"code":"key219", "msg":"Invalid parameter to display_template %s", "values": ["%s"]}""" % (self.name, 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("""{"code":"key41", "msg":"Unable to parse 'position' in section '%s'", "values": ["%s"]}"""
|
||||
% (c.get_name(), 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("""{"code":"key220", "msg":"Unknown display_data group '%s'", "values": ["%s"]}""" % (group,group))
|
||||
self.show_data_group = new_dg
|
||||
|
||||
def load_config(config):
|
||||
return PrinterLCD(config)
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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("""{"code":"key224", "msg":"hd44780 all pins must be on same mcu'", "values": []}""")
|
||||
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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,782 @@
|
||||
# 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
|
||||
# + Z Tilt
|
||||
# + Quad Gantry Lvl
|
||||
# + Bed Mesh
|
||||
# + 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 __z_tilt]
|
||||
type: command
|
||||
enable: {not printer.idle_timeout.state == "Printing" and ('z_tilt' in printer)}
|
||||
name: Z Tilt
|
||||
gcode: Z_TILT_ADJUST
|
||||
|
||||
[menu __main __control __quad_gantry_level]
|
||||
type: command
|
||||
enable: {not printer.idle_timeout.state == "Printing" and ('quad_gantry_level' in printer)}
|
||||
name: Quad Gantry Lvl
|
||||
gcode: QUAD_GANTRY_LEVEL
|
||||
|
||||
[menu __main __control __bed_mesh]
|
||||
type: command
|
||||
enable: {not printer.idle_timeout.state == "Printing" and ('bed_mesh' in printer)}
|
||||
name: Bed Mesh
|
||||
gcode: BED_MESH_CALIBRATE
|
||||
|
||||
[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") and ('delta_calibrate' in printer)}
|
||||
name: Delta cal. auto
|
||||
gcode:
|
||||
G28
|
||||
DELTA_CALIBRATE
|
||||
|
||||
[menu __main __setup __calib __delta_calib_man]
|
||||
type: list
|
||||
enable: {(not printer.idle_timeout.state == "Printing") and ('delta_calibrate' in printer)}
|
||||
name: Delta cal. man
|
||||
|
||||
[menu __main __setup __calib __bedprobe]
|
||||
type: command
|
||||
enable: {(not printer.idle_timeout.state == "Printing") and ('probe' in printer)}
|
||||
name: Bed probe
|
||||
gcode: PROBE
|
||||
|
||||
[menu __main __setup __calib __delta_calib_man __start]
|
||||
type: command
|
||||
name: Start probing
|
||||
gcode:
|
||||
G28
|
||||
DELTA_CALIBRATE METHOD=manual
|
||||
|
||||
[menu __main __setup __calib __delta_calib_man __move_z]
|
||||
type: input
|
||||
name: Move Z: {'%03.2f' % menu.input}
|
||||
input: {printer.gcode_move.gcode_position.z}
|
||||
input_step: 1
|
||||
realtime: True
|
||||
gcode:
|
||||
{%- if menu.event == 'change' -%}
|
||||
G1 Z{'%.2f' % menu.input}
|
||||
{%- elif menu.event == 'long_click' -%}
|
||||
G1 Z{'%.2f' % menu.input}
|
||||
SAVE_GCODE_STATE NAME=__move__axis
|
||||
G91
|
||||
G1 Z2
|
||||
G1 Z-2
|
||||
RESTORE_GCODE_STATE NAME=__move__axis
|
||||
{%- endif -%}
|
||||
|
||||
[menu __main __setup __calib __delta_calib_man __test_z]
|
||||
type: input
|
||||
name: Test Z: {['++','+','+.01','+.05','+.1','+.5','-.5','-.1','-.05','-.01','-','--'][menu.input|int]}
|
||||
input: 6
|
||||
input_min: 0
|
||||
input_max: 11
|
||||
input_step: 1
|
||||
gcode:
|
||||
{%- if menu.event == 'long_click' -%}
|
||||
TESTZ Z={['++','+','+.01','+.05','+.1','+.5','-.5','-.1','-.05','-.01','-','--'][menu.input|int]}
|
||||
{%- endif -%}
|
||||
|
||||
[menu __main __setup __calib __delta_calib_man __accept]
|
||||
type: command
|
||||
name: Accept
|
||||
gcode: ACCEPT
|
||||
|
||||
[menu __main __setup __calib __delta_calib_man __abort]
|
||||
type: command
|
||||
name: Abort
|
||||
gcode: ABORT
|
||||
|
||||
[menu __main __setup __dump]
|
||||
type: command
|
||||
name: Dump parameters
|
||||
gcode:
|
||||
{% for name1 in printer %}
|
||||
{% for name2 in printer[name1] %}
|
||||
{ action_respond_info("printer['%s'].%s = %s"
|
||||
% (name1, name2, printer[name1][name2])) }
|
||||
{% else %}
|
||||
{ action_respond_info("printer['%s'] = %s" % (name1, printer[name1])) }
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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("""{"code":"key230", "msg":"Unable to parse encoder_pins", "values": []}""")
|
||||
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("""{"code":"key190", "msg": "Shutdown due to kill button!", "values": []}""")
|
||||
@@ -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("""{"code":"key105", "msg": "st7920 all pins must be on same mcu", "values": []}""")
|
||||
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("""{"code":"key231", "msg":"%s spi pins must be on same mcu", "values": ["%s"]}""" % (
|
||||
config.get_name(), 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))
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,237 @@
|
||||
# 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", "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(
|
||||
"""{"code":"key157", "msg": "Invalid trigger_phase '%s'", "values": ["%s"]}""" % (trigger_phase, 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(
|
||||
"""{"code":"key158", "msg": "Endstop for %s is not accurate enough for stepper phase adjustment", "values": ["%s"]}""" % (
|
||||
self.name, 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(
|
||||
"""{"code":"key161", "msg": "Endstop %s incorrect phase (got %d vs %d)", "values": ["%s", %d, %d]}""" % (
|
||||
self.name, phase, self.endstop_phase, 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)
|
||||
@@ -0,0 +1,311 @@
|
||||
# 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)
|
||||
self.gcode.register_command('EXCLUDE_OBJECT_RESET', self.cmd_EXCLUDE_OBJECT_RESET)
|
||||
def cmd_EXCLUDE_OBJECT_RESET(self, gcmd):
|
||||
if self.objects:
|
||||
self.gcode.run_script_from_command("M400")
|
||||
self.gcode.run_script_from_command("EXCLUDE_OBJECT_DEFINE RESET=1")
|
||||
self.gcode.run_script_from_command("M400")
|
||||
|
||||
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 name == self.current_object:
|
||||
self.gcode.respond_info("Forbidden EXCLUDE_OBJECT current_print_object:%s" % self.current_object)
|
||||
return
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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 get_status(self, eventtime):
|
||||
return self.extruder_stepper.get_status(eventtime)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterExtruderStepper(config)
|
||||
@@ -0,0 +1,121 @@
|
||||
# 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)
|
||||
|
||||
self.enable_pin = None
|
||||
enable_pin = config.get('enable_pin', None)
|
||||
if enable_pin is not None:
|
||||
self.enable_pin = ppins.setup_pin('digital_out', enable_pin)
|
||||
self.enable_pin.setup_max_duration(0.)
|
||||
|
||||
# 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 self.enable_pin:
|
||||
if value > 0 and self.last_fan_value == 0:
|
||||
self.enable_pin.set_digital(print_time, 1)
|
||||
elif value == 0 and self.last_fan_value > 0:
|
||||
self.enable_pin.set_digital(print_time, 0)
|
||||
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)
|
||||
@@ -0,0 +1,116 @@
|
||||
import logging
|
||||
|
||||
|
||||
class FanFeedback:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
|
||||
self.print_delay_time = config.getfloat('print_delay_time', 5.)
|
||||
self.current_delay_time = config.getfloat('current_delay_time', 2.)
|
||||
|
||||
ppins = self.printer.lookup_object('pins')
|
||||
|
||||
self.params = []
|
||||
fan_num = 0
|
||||
for i in range(0, 5):
|
||||
sensor_pin = config.get("fan%d_pin" % i, None)
|
||||
# logging.info("fan feedback sensor_pin: %s" % sensor_pin)
|
||||
if not sensor_pin:
|
||||
continue
|
||||
pin_params = ppins.lookup_pin(sensor_pin, can_invert=False, can_pullup=True)
|
||||
mcu = pin_params['chip']
|
||||
oid = mcu.create_oid()
|
||||
config_cmd = "config_fancheck oid=%d fan_num=%d fan0_pin=%s pull_up0=%s" \
|
||||
" fan1_pin=%s pull_up1=%s fan2_pin=%s pull_up2=%s fan3_pin=%s" \
|
||||
" pull_up3=%s fan4_pin=%s pull_up4=%s" % (
|
||||
oid, 5,
|
||||
pin_params['pin'], pin_params["pullup"],
|
||||
pin_params['pin'], pin_params["pullup"],
|
||||
pin_params['pin'], pin_params["pullup"],
|
||||
pin_params['pin'], pin_params["pullup"],
|
||||
pin_params['pin'], pin_params["pullup"]
|
||||
)
|
||||
if fan_num == 0:
|
||||
mcu.register_response(self._handle_result_fan_check0, "fan_status", oid)
|
||||
elif fan_num == 1:
|
||||
mcu.register_response(self._handle_result_fan_check1, "fan_status", oid)
|
||||
fan_num += 1
|
||||
param = i, config_cmd, pin_params, mcu, oid
|
||||
# logging.info("%s" % (config_cmd))
|
||||
mcu.add_config_cmd(config_cmd)
|
||||
self.params.append(param)
|
||||
self.which_fan = 2**fan_num - 1
|
||||
self.fan_num = fan_num
|
||||
|
||||
self.gcode = config.get_printer().lookup_object('gcode')
|
||||
self.gcode.register_command("QUERY_FAN_CHECK", self.cmd_QUERY_FAN_CHECK, desc=self.cmd_QUERY_FAN_CHECK_help)
|
||||
self.print_stats = self.printer.load_object(config, 'print_stats')
|
||||
self.printer.register_event_handler("klippy:ready", self.handle_ready)
|
||||
self.cx_fan_status = {}
|
||||
webhooks = self.printer.lookup_object('webhooks')
|
||||
webhooks.register_endpoint("get_cx_fan_status",
|
||||
self._get_cx_fan_status)
|
||||
|
||||
def handle_ready(self):
|
||||
reactor = self.printer.get_reactor()
|
||||
reactor.register_timer(
|
||||
self.cx_fan_status_update_event, reactor.monotonic()+1.)
|
||||
|
||||
def delay_s(self, delay_s):
|
||||
toolhead = self.printer.lookup_object("toolhead")
|
||||
reactor = self.printer.get_reactor()
|
||||
eventtime = reactor.monotonic()
|
||||
if not self.printer.is_shutdown():
|
||||
toolhead.get_last_move_time()
|
||||
eventtime = reactor.pause(eventtime + delay_s)
|
||||
pass
|
||||
|
||||
def _get_cx_fan_status(self):
|
||||
return self.cx_fan_status
|
||||
|
||||
def cx_fan_status_update_event(self, eventtime):
|
||||
if self.print_stats.get_status(eventtime).get("state") != "printing":
|
||||
next_time = eventtime + self.current_delay_time
|
||||
else:
|
||||
next_time = eventtime + self.print_delay_time
|
||||
for i in self.params:
|
||||
cmd = "query_fancheck oid=%c which_fan=%c"
|
||||
oid = i[4]
|
||||
mcu = i[3]
|
||||
query_cmd = mcu.lookup_command(cmd, cq=None)
|
||||
# log_cmd = "query_fancheck oid=%s which_fan=%s" % (oid, 31)
|
||||
# logging.info("%s" % log_cmd)
|
||||
query_cmd.send([oid, self.which_fan])
|
||||
return next_time
|
||||
|
||||
cmd_QUERY_FAN_CHECK_help = "Check CXSW Special Fan Status"
|
||||
def cmd_QUERY_FAN_CHECK(self, gcmd):
|
||||
self.gcode.respond_info("%s" % self.cx_fan_status)
|
||||
|
||||
def _handle_result_fan_check0(self, params):
|
||||
# logging.info("_handle_result_fan_check0: %s" % params)
|
||||
# self.cx_fan_status["fan0_speed"] = params.get("fan0_speed", 0)
|
||||
self.cx_fan_status = {
|
||||
"fan0_speed": params.get("fan0_speed", 0),
|
||||
"fan1_speed": self.cx_fan_status.get("fan1_speed", 0),
|
||||
"fan2_speed": self.cx_fan_status.get("fan2_speed", 0),
|
||||
"fan3_speed": self.cx_fan_status.get("fan3_speed", 0),
|
||||
"fan4_speed": self.cx_fan_status.get("fan4_speed", 0),
|
||||
}
|
||||
|
||||
def _handle_result_fan_check1(self, params):
|
||||
# logging.info("_handle_result_fan_check1: %s" % params)
|
||||
# self.cx_fan_status["fan1_speed"] = params.get("fan1_speed", 0)
|
||||
self.cx_fan_status = {
|
||||
"fan0_speed": self.cx_fan_status.get("fan0_speed", 0),
|
||||
"fan1_speed": params.get("fan1_speed", 0),
|
||||
"fan2_speed": self.cx_fan_status.get("fan2_speed", 0),
|
||||
"fan3_speed": self.cx_fan_status.get("fan3_speed", 0),
|
||||
"fan4_speed": self.cx_fan_status.get("fan4_speed", 0),
|
||||
}
|
||||
|
||||
def get_status(self, eventtime):
|
||||
return self.cx_fan_status
|
||||
|
||||
def load_config(config):
|
||||
return FanFeedback(config)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,120 @@
|
||||
# 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")
|
||||
print_stats = self.printer.lookup_object('print_stats')
|
||||
is_printing = print_stats.state == "printing"
|
||||
# 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)
|
||||
@@ -0,0 +1,125 @@
|
||||
# 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.
|
||||
from logging import Filter
|
||||
from os import remove
|
||||
from time import time
|
||||
import mcu
|
||||
import math
|
||||
|
||||
|
||||
class RCTFilter:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def ftr_val(self, vals):
|
||||
out_vals = []
|
||||
if len(vals) < 3:
|
||||
return vals
|
||||
for i in range(len(vals) - 2):
|
||||
tmp = [math.fabs(vals[i]), math.fabs(vals[i + 1]), math.fabs(vals[i + 2])]
|
||||
index = tmp.index(min(tmp))
|
||||
out_vals.append(vals[index + i])
|
||||
out_vals.append(vals[-2])
|
||||
out_vals.append(vals[-1])
|
||||
return out_vals
|
||||
|
||||
|
||||
class RCHFilter:
|
||||
def __init__(self, cut_frq_hz, acq_frq_hz):
|
||||
self.cut_frq_hz = cut_frq_hz
|
||||
self.acq_frq_hz = acq_frq_hz
|
||||
pass
|
||||
|
||||
def ftr_val(self, vals):
|
||||
out_vals = [0]
|
||||
rc = 1. / 2. / math.pi / self.cut_frq_hz
|
||||
coff = rc / (rc + 1. / self.acq_frq_hz)
|
||||
for i in range(1, len(vals)):
|
||||
out_vals.append((vals[i] - vals[i - 1] + out_vals[-1]) * coff)
|
||||
return out_vals
|
||||
|
||||
|
||||
class RCLFilter:
|
||||
def __init__(self, k1_new):
|
||||
self.k1_new = k1_new
|
||||
pass
|
||||
|
||||
def ftr_val(self, vals):
|
||||
out_vals = [vals[0]]
|
||||
for i in range(1, len(vals)):
|
||||
out_vals.append(out_vals[-1] * (1 - self.k1_new) + vals[i] * self.k1_new)
|
||||
return out_vals
|
||||
|
||||
|
||||
class Filter:
|
||||
def __init__(self, config):
|
||||
self.hft_hz = config.getfloat('hft_hz', default=5, minval=0.1, maxval=10.)
|
||||
self.lft_k1 = config.getfloat('lft_k1', default=0.8, minval=0., maxval=1.)
|
||||
self.lft_k1_oft = config.getfloat('lft_k1_oft', default=0.8, minval=0., maxval=1.)
|
||||
self.lft_k1_cal = config.getfloat('lft_k1_cal', default=0.8, minval=0., maxval=1.)
|
||||
pass
|
||||
|
||||
def get_tft(self):
|
||||
return RCTFilter()
|
||||
|
||||
def get_lft(self, k1):
|
||||
return RCLFilter(k1)
|
||||
|
||||
def get_hft(self, cut_hz, acq_hz):
|
||||
return RCHFilter(cut_frq_hz=cut_hz, acq_frq_hz=acq_hz)
|
||||
|
||||
def cal_offset_by_vals(self, s_count, new_valss, lft_k1, cut_len):
|
||||
out_vals = []
|
||||
tmp_vals = [[], [], [], []]
|
||||
tft = RCTFilter()
|
||||
lft = RCLFilter(lft_k1)
|
||||
for i in range(s_count):
|
||||
tmp_vals[i] = tft.ftr_val(new_valss[i])
|
||||
tmp_vals[i] = lft.ftr_val(tmp_vals[i])
|
||||
for i in range(len(tmp_vals[0])):
|
||||
sums = 0
|
||||
for j in range(s_count):
|
||||
if i < len(tmp_vals[j]):
|
||||
sums += math.fabs(tmp_vals[j][i])
|
||||
out_vals.append(sums)
|
||||
if len(out_vals) > cut_len:
|
||||
del out_vals[0:(len(out_vals) - cut_len)]
|
||||
for i in range(s_count):
|
||||
if len(tmp_vals[i]) > cut_len:
|
||||
del tmp_vals[i][0:(len(tmp_vals[i]) - cut_len)]
|
||||
for j in range(len(tmp_vals[i])):
|
||||
tmp_vals[i][j] = abs(tmp_vals[i][j])
|
||||
return out_vals, tmp_vals
|
||||
|
||||
|
||||
def cal_filter_by_vals(self, s_count, now_valss, hft_hz, lft_k1, cut_len):
|
||||
out_vals = []
|
||||
tmp_vals = [[], [], [], []]
|
||||
tft = RCTFilter()
|
||||
hft = RCHFilter(hft_hz, 80)
|
||||
lft = RCLFilter(lft_k1)
|
||||
for i in range(0, s_count):
|
||||
tmp_vals[i] = tft.ftr_val(now_valss[i])
|
||||
tmp_vals[i] = hft.ftr_val(tmp_vals[i])
|
||||
tmp_vals[i] = lft.ftr_val(tmp_vals[i])
|
||||
for i in range(len(tmp_vals[0])):
|
||||
sums = 0
|
||||
for j in range(s_count):
|
||||
if i < len(tmp_vals[j]):
|
||||
sums += math.fabs(tmp_vals[j][i])
|
||||
out_vals.append(sums)
|
||||
if len(out_vals) > cut_len:
|
||||
del out_vals[0:(len(out_vals) - cut_len)]
|
||||
for i in range(s_count):
|
||||
if len(tmp_vals[i]) > cut_len:
|
||||
del tmp_vals[i][0:(len(tmp_vals[i]) - cut_len)]
|
||||
for j in range(len(tmp_vals[i])):
|
||||
tmp_vals[i][j] = abs(tmp_vals[i][j])
|
||||
return out_vals, tmp_vals
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return Filter(config)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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)
|
||||
if config.getboolean("enable_force_move", False):
|
||||
gcode.register_command('FORCE_MOVE', self.cmd_FORCE_MOVE,
|
||||
desc=self.cmd_FORCE_MOVE_help)
|
||||
gcode.register_command('SET_KINEMATIC_POSITION',
|
||||
self.cmd_SET_KINEMATIC_POSITION,
|
||||
desc=self.cmd_SET_KINEMATIC_POSITION_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("""{"code":"key31", "msg":"Unknown stepper %s", "values": ["%s"]}""" % (name, 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("""{"code":"key31", "msg":"Unknown stepper %s", "values": ["%s"]}""" % (name, 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)
|
||||
@@ -0,0 +1,181 @@
|
||||
# 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.
|
||||
#
|
||||
# supports XY, XZ & YZ planes with remaining axis as helical
|
||||
|
||||
# Enum
|
||||
ARC_PLANE_X_Y = 0
|
||||
ARC_PLANE_X_Z = 1
|
||||
ARC_PLANE_Y_Z = 2
|
||||
|
||||
# Enum
|
||||
X_AXIS = 0
|
||||
Y_AXIS = 1
|
||||
Z_AXIS = 2
|
||||
E_AXIS = 3
|
||||
|
||||
|
||||
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)
|
||||
|
||||
self.gcode.register_command("G17", self.cmd_G17)
|
||||
self.gcode.register_command("G18", self.cmd_G18)
|
||||
self.gcode.register_command("G19", self.cmd_G19)
|
||||
|
||||
self.Coord = self.gcode.Coord
|
||||
|
||||
# backwards compatibility, prior implementation only supported XY
|
||||
self.plane = ARC_PLANE_X_Y
|
||||
|
||||
def cmd_G2(self, gcmd):
|
||||
self._cmd_inner(gcmd, True)
|
||||
|
||||
def cmd_G3(self, gcmd):
|
||||
self._cmd_inner(gcmd, False)
|
||||
|
||||
def cmd_G17(self, gcmd):
|
||||
self.plane = ARC_PLANE_X_Y
|
||||
|
||||
def cmd_G18(self, gcmd):
|
||||
self.plane = ARC_PLANE_X_Z
|
||||
|
||||
def cmd_G19(self, gcmd):
|
||||
self.plane = ARC_PLANE_Y_Z
|
||||
|
||||
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
|
||||
asTarget = self.Coord(x=gcmd.get_float("X", currentPos[0]),
|
||||
y=gcmd.get_float("Y", currentPos[1]),
|
||||
z=gcmd.get_float("Z", currentPos[2]),
|
||||
e=None)
|
||||
|
||||
if gcmd.get_float("R", None) is not None:
|
||||
raise gcmd.error("G2/G3 does not support R moves")
|
||||
|
||||
# determine the plane coordinates and the helical axis
|
||||
asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('IJ') ]
|
||||
axes = (X_AXIS, Y_AXIS, Z_AXIS)
|
||||
if self.plane == ARC_PLANE_X_Z:
|
||||
asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('IK') ]
|
||||
axes = (X_AXIS, Z_AXIS, Y_AXIS)
|
||||
elif self.plane == ARC_PLANE_Y_Z:
|
||||
asPlanar = [ gcmd.get_float(a, 0.) for i,a in enumerate('JK') ]
|
||||
axes = (Y_AXIS, Z_AXIS, X_AXIS)
|
||||
|
||||
if not (asPlanar[0] or asPlanar[1]):
|
||||
raise gcmd.error("G2/G3 requires IJ, IK or JK parameters")
|
||||
|
||||
asE = gcmd.get_float("E", None)
|
||||
asF = gcmd.get_float("F", None)
|
||||
|
||||
# Build list of linear coordinates to move
|
||||
coords = self.planArc(currentPos, asTarget, asPlanar,
|
||||
clockwise, *axes)
|
||||
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
|
||||
#
|
||||
# alpha and beta axes are the current plane, helical axis is linear travel
|
||||
def planArc(self, currentPos, targetPos, offset, clockwise,
|
||||
alpha_axis, beta_axis, helical_axis):
|
||||
# todo: sometimes produces full circles
|
||||
|
||||
# Radius vector from center to current location
|
||||
r_P = -offset[0]
|
||||
r_Q = -offset[1]
|
||||
|
||||
# Determine angular travel
|
||||
center_P = currentPos[alpha_axis] - r_P
|
||||
center_Q = currentPos[beta_axis] - r_Q
|
||||
rt_Alpha = targetPos[alpha_axis] - center_P
|
||||
rt_Beta = targetPos[beta_axis] - center_Q
|
||||
angular_travel = math.atan2(r_P * rt_Beta - r_Q * rt_Alpha,
|
||||
r_P * rt_Alpha + r_Q * rt_Beta)
|
||||
if angular_travel < 0.:
|
||||
angular_travel += 2. * math.pi
|
||||
if clockwise:
|
||||
angular_travel -= 2. * math.pi
|
||||
|
||||
if (angular_travel == 0.
|
||||
and currentPos[alpha_axis] == targetPos[alpha_axis]
|
||||
and currentPos[beta_axis] == targetPos[beta_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[helical_axis] - currentPos[helical_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_Helical = 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
|
||||
|
||||
# Coord doesn't support index assignment, create list
|
||||
c = [None, None, None, None]
|
||||
c[alpha_axis] = center_P + r_P
|
||||
c[beta_axis] = center_Q + r_Q
|
||||
c[helical_axis] = currentPos[helical_axis] + dist_Helical
|
||||
coords.append(self.Coord(*c))
|
||||
|
||||
coords.append(targetPos)
|
||||
return coords
|
||||
|
||||
def load_config(config):
|
||||
return ArcSupport(config)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,221 @@
|
||||
# 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])
|
||||
msg = """{"code":"key164", "msg": "Error loading template '%s': %s", "values": ["%s", "%s"]}""" % (
|
||||
name, traceback.format_exception_only(type(e), e)[-1], 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])
|
||||
msg = """{"code":"key165", "msg": "Error evaluating '%s': %s", "values": ["%s", "%s"]}""" % (
|
||||
self.name, traceback.format_exception_only(type(e), e)[-1],
|
||||
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("""{"code":"key170", "msg": "Shutdown due to %s", "values": ["%s"]}""" % (msg, 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())
|
||||
"""{"code":"key166", "msg": "Name of section '%s' contains illegal whitespace", "values": ["%s"]}""" % (
|
||||
config.get_name(), 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')"
|
||||
"""{"code":"key167", "msg": "G-Code macro rename of different types ('%s' vs '%s')", "values": ["%s", "%s"]}"""
|
||||
% (self.alias, self.rename_existing, 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(
|
||||
"""{"code":"key169", "msg": "Existing command '%s' not found in gcode_macro rename", "values": ["%s"]}""" % (
|
||||
self.alias, 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
|
||||
try:
|
||||
import os, json
|
||||
if "z_safe_pause" in variable:
|
||||
logging.info("SET_GCODE_VARIABLE variable:%s literal:%s" % (variable, literal))
|
||||
v_sd = self.printer.lookup_object('virtual_sdcard', None)
|
||||
if os.path.exists(v_sd.print_file_name_path):
|
||||
result = {}
|
||||
with open(v_sd.print_file_name_path, "r") as f:
|
||||
result = (json.loads(f.read()))
|
||||
result["variable_z_safe_pause"] = literal
|
||||
with open(v_sd.print_file_name_path, "w") as f:
|
||||
f.write(json.dumps(result))
|
||||
f.flush()
|
||||
except Exception as err:
|
||||
logging.error("SET_GCODE_VARIABLE save z_safe_pause err:%s" % err)
|
||||
def cmd(self, gcmd):
|
||||
if self.in_script:
|
||||
# raise gcmd.error("Macro %s called recursively" % (self.alias,))
|
||||
raise gcmd.error("""{"code":"key172", "msg": "Macro %s called recursively", "values": ["%s"]}""" % (self.alias, 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)
|
||||
@@ -0,0 +1,493 @@
|
||||
# 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()
|
||||
self.variable_safe_z = 0
|
||||
if config.has_section('gcode_macro PRINTER_PARAM'):
|
||||
PRINTER_PARAM = config.getsection('gcode_macro PRINTER_PARAM')
|
||||
try:
|
||||
self.variable_safe_z = PRINTER_PARAM.getfloat('variable_z_safe_g28')
|
||||
except:
|
||||
section = "bltouch"
|
||||
if config.has_section(section):
|
||||
logging.info("with bltouch")
|
||||
self.variable_safe_z = PRINTER_PARAM.getfloat('variable_z_safe_g28_touch', 0.0)
|
||||
else:
|
||||
logging.info("no bltouch")
|
||||
tmp = PRINTER_PARAM.getfloat('variable_z_safe_g28_no_touch', 0.0)
|
||||
if tmp is not None:
|
||||
self.variable_safe_z = tmp
|
||||
logging.info("self.variable_safe_z = %s" % self.variable_safe_z)
|
||||
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)
|
||||
gcode.register_command('SET_POSITION', self.cmd_SET_POSITION, True, desc=self.cmd_SET_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("""{"code":"key272": "msg":"Invalid speed in '%s'", "values":["%s"]}"""
|
||||
% (gcmd.get_commandline(),gcmd.get_commandline()))
|
||||
self.speed = gcode_speed * self.speed_factor
|
||||
except ValueError as e:
|
||||
raise gcmd.error("""{"code":"key273": "msg":"Unable to parse move '%s'", "values":["%s"]}"""
|
||||
% (gcmd.get_commandline(),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)
|
||||
def recordPrintFileName(self, path, file_name, fan_state="", filament_used=0, last_print_duration=0, pressure_advance="", slow_print=False):
|
||||
import json, os
|
||||
fan = {}
|
||||
M204_accel = ""
|
||||
old_filament_used = 0
|
||||
old_last_print_duration = 0
|
||||
old_pressure_advance = ""
|
||||
last_speed_factor = 0.0166666
|
||||
last_speed = 25
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as f:
|
||||
result = (json.loads(f.read()))
|
||||
# fan = result.get("fan_state", "")
|
||||
fan = result.get("fan_state", {})
|
||||
M204_accel = result.get("M204", "")
|
||||
old_filament_used = result.get("filament_used", 0)
|
||||
old_last_print_duration = result.get("last_print_duration", 0)
|
||||
old_pressure_advance = result.get("pressure_advance", "")
|
||||
last_speed_factor = result.get("speed_factor", 0.0166666)
|
||||
last_speed = result.get("speed", 25)
|
||||
if fan_state.startswith("M106 S"):
|
||||
fan["M106 S"] = fan_state
|
||||
elif fan_state.startswith("M106 P0"):
|
||||
fan["M106 P0"] = fan_state
|
||||
elif fan_state.startswith("M106 P1"):
|
||||
fan["M106 P1"] = fan_state
|
||||
elif fan_state.startswith("M106 P2"):
|
||||
fan["M106 P2"] = fan_state
|
||||
# if fan_state:
|
||||
# fan.append(fan_state)
|
||||
# if fan_state and fan_state != fan:
|
||||
# state = fan_state
|
||||
# else:
|
||||
# state = fan
|
||||
if filament_used and filament_used != old_filament_used:
|
||||
pass
|
||||
else:
|
||||
filament_used = old_filament_used
|
||||
if last_print_duration and last_print_duration != old_last_print_duration:
|
||||
pass
|
||||
else:
|
||||
last_print_duration = old_last_print_duration
|
||||
if pressure_advance and pressure_advance != old_pressure_advance:
|
||||
pass
|
||||
else:
|
||||
pressure_advance = old_pressure_advance
|
||||
data = {
|
||||
'file_path': file_name,
|
||||
'absolute_coord': self.absolute_coord,
|
||||
'absolute_extrude': self.absolute_extrude,
|
||||
'extrude_factor': self.extrude_factor,
|
||||
# 'fan_state': state,
|
||||
'speed_factor': last_speed_factor if slow_print else self.speed_factor,
|
||||
"speed": last_speed if slow_print else self.speed,
|
||||
'fan_state': fan,
|
||||
'M204': M204_accel,
|
||||
'filament_used': filament_used,
|
||||
'last_print_duration': last_print_duration,
|
||||
'pressure_advance': pressure_advance
|
||||
}
|
||||
with open(path, "w") as f:
|
||||
f.write(json.dumps(data))
|
||||
f.flush()
|
||||
cmd_CX_RESTORE_GCODE_STATE_help = "Restore a previously saved G-Code state"
|
||||
def cmd_CX_RESTORE_GCODE_STATE(self, print_info, file_name_path, XYZE):
|
||||
try:
|
||||
state = {
|
||||
"absolute_extrude": True,
|
||||
"file_position": 0,
|
||||
"extrude_factor": 1.0,
|
||||
"speed_factor": 0.0166666,
|
||||
"homing_position": [0.0, 0.0, 0.0, 0.0],
|
||||
"last_position": [0.0, 0.0, 0.0, 0.0],
|
||||
"speed": 25.0,
|
||||
"file_path": "",
|
||||
"base_position": [0.0, 0.0, 0.0, -0.0],
|
||||
"absolute_coord": True,
|
||||
# "fan_state": "",
|
||||
"fan_state": {},
|
||||
"variable_z_safe_pause": 0,
|
||||
"M204": "",
|
||||
"filament_used": 0,
|
||||
"last_print_duration": 0,
|
||||
"pressure_advance": "",
|
||||
"z_toolhead_moved": 0
|
||||
}
|
||||
import os, json
|
||||
base_position_e = -1
|
||||
state["file_position"] = print_info.get("file_position", 0)
|
||||
state["base_position"] = [0.0, 0.0, 0.0, print_info.get("base_position_e", -1)]
|
||||
base_position_e = print_info.get("base_position_e", -1)
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE base_position_e:%s" % base_position_e)
|
||||
with open(file_name_path, "r") as f:
|
||||
file_info = json.loads(f.read())
|
||||
state["file_path"] = file_info.get("file_path", "")
|
||||
state["absolute_extrude"] = file_info.get("absolute_extrude", True)
|
||||
state["absolute_coord"] = file_info.get("absolute_coord", True)
|
||||
state["fan_state"] = file_info.get("fan_state", {})
|
||||
state["variable_z_safe_pause"] = file_info.get("variable_z_safe_pause", 0)
|
||||
state["M204"] = file_info.get("M204", "")
|
||||
state["speed_factor"] = file_info.get("speed_factor", 0.016666666)
|
||||
state["extrude_factor"] = file_info.get("extrude_factor", 1.0)
|
||||
state["speed"] = file_info.get("speed", 25)
|
||||
state["pressure_advance"] = file_info.get("pressure_advance", "")
|
||||
state["z_toolhead_moved"] = file_info.get("z_toolhead_moved", 0)
|
||||
state["last_position"] = [XYZE["X"], XYZE["Y"], XYZE["Z"], XYZE["E"]+base_position_e]
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE state:%s" % str(state))
|
||||
|
||||
# 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']
|
||||
self.extrude_factor = 1.0
|
||||
# Restore the relative E position
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE base_position:%s" % str(self.base_position))
|
||||
e_diff = self.last_position[3] - state['last_position'][3] + 1.0
|
||||
self.base_position[3] += e_diff
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE self.last_position[3]:%s, state['last_position'][3]:%s, e_diff:%s, \
|
||||
base_position[3]:%s" % (self.last_position[3], state['last_position'][3], e_diff, self.base_position[3]))
|
||||
# Move the toolhead back if requested
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
if state["fan_state"]:
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE fan fan_state:%s" % str(state["fan_state"]))
|
||||
for key in state["fan_state"]:
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE fan set fan:%s#" % str(state["fan_state"].get(key, "")))
|
||||
gcode.run_script_from_command(state["fan_state"].get(key, ""))
|
||||
# gcode.run_script_from_command(state["fan_state"])
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE before G28 X Y self.last_position:%s" % str(self.last_position))
|
||||
gcode.run_script_from_command("SOFT_CHECK_ERROR FLAG=1")
|
||||
gcode.run_script_from_command("G28 X Y")
|
||||
gcode.run_script_from_command("SOFT_CHECK_ERROR FLAG=0")
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE after G28 X Y self.last_position:%s" % str(self.last_position))
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE before BED_MESH_PROFILE LOAD='default'")
|
||||
gcode.run_script_from_command('BED_MESH_PROFILE LOAD="default"')
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE after BED_MESH_PROFILE LOAD='default'")
|
||||
x = self.last_position[0]
|
||||
y = self.last_position[1]
|
||||
z = state['last_position'][2] + self.variable_safe_z + state["variable_z_safe_pause"] + state["z_toolhead_moved"]
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE self.last_position[2]:%s, state['last_position'][2]:%s, self.variable_safe_z:%s, \
|
||||
state['variable_z_safe_pause']:%s" % (self.last_position[2], state['last_position'][2], self.variable_safe_z, state["variable_z_safe_pause"]))
|
||||
toolhead = self.printer.lookup_object("toolhead")
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE toolhead.set_position:%s" % str([x, y, z, self.last_position[3]]))
|
||||
toolhead.set_position([x, y, z, self.last_position[3]], homing_axes=(2,))
|
||||
speed = self.speed
|
||||
self.last_position[:3] = state['last_position'][:3]
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE G1 X%s Y%s F2400" % (state['last_position'][0], state['last_position'][1]))
|
||||
gcode.run_script_from_command("G1 X%s Y%s F2400" % (state['last_position'][0], state['last_position'][1]))
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE move_with_transform:%s, speed:%s" % (self.last_position, speed))
|
||||
self.move_with_transform(self.last_position, speed)
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE G1 X%s Y%s F3000" % (state['last_position'][0], state['last_position'][1]))
|
||||
gcode.run_script_from_command("G1 X%s Y%s F3000" % (state['last_position'][0], state['last_position'][1]))
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE M400")
|
||||
gcode.run_script_from_command("G1 X%s Y%s F%s" % (state['last_position'][0], state['last_position'][1], int(state['speed']/state['speed_factor'])))
|
||||
logging.info("power_loss RESTORE F%s" % (int(state['speed']/state['speed_factor'])))
|
||||
gcode.run_script_from_command("M400")
|
||||
if state["M204"]:
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE SET M204:%s#" % state["M204"])
|
||||
gcode.run_script_from_command(state["M204"])
|
||||
if state["pressure_advance"]:
|
||||
gcode.run_script_from_command(state["pressure_advance"])
|
||||
self.absolute_extrude = state['absolute_extrude']
|
||||
gcode.run_script_from_command("M221 S%s" % int(state['extrude_factor']*100))
|
||||
try:
|
||||
if os.path.exists(gcode.exclude_object_info):
|
||||
with open(gcode.exclude_object_info, "r") as f:
|
||||
exclude_object_cmds = json.loads(f.read())
|
||||
EXCLUDE_OBJECT_DEFINE = exclude_object_cmds.get("EXCLUDE_OBJECT_DEFINE", [])
|
||||
EXCLUDE_OBJECT = exclude_object_cmds.get("EXCLUDE_OBJECT", [])
|
||||
for line in EXCLUDE_OBJECT_DEFINE:
|
||||
gcode.run_script_from_command(line)
|
||||
for line in EXCLUDE_OBJECT:
|
||||
gcode.run_script_from_command(line)
|
||||
gcode.run_script_from_command("M400")
|
||||
except Exception as err:
|
||||
logging.exception("RESTORE EXCLUDE_OBJECT err:%s" % err)
|
||||
logging.info("power_loss cmd_CX_RESTORE_GCODE_STATE done")
|
||||
except Exception as err:
|
||||
logging.exception("cmd_CX_RESTORE_GCODE_STATE err:%s" % err)
|
||||
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("""{"code":"key274", "msg": "Unknown g-code state: %s", "values":["%s"]}""" % (state_name, 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("""{"code": "key283", "msg": ""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))
|
||||
cmd_SET_POSITION_help = (
|
||||
"SET_POSITION information on the current location of the toolhead")
|
||||
def cmd_SET_POSITION(self, gcmd):
|
||||
toolhead = self.printer.lookup_object('toolhead', None)
|
||||
if toolhead is None:
|
||||
raise gcmd.error("""{"code": "key283", "msg": ""Printer not ready"}""")
|
||||
position = toolhead.get_position()
|
||||
x = position[0]
|
||||
y = position[1]
|
||||
z = position[2]
|
||||
e = position[3]
|
||||
X = gcmd.get_float('X', x)
|
||||
Y = gcmd.get_float('Y', y)
|
||||
Z = gcmd.get_float('Z', z)
|
||||
E = gcmd.get_float('E', e)
|
||||
toolhead.set_position([X, Y, Z, E], homing_axes=(2,))
|
||||
position = toolhead.get_position()
|
||||
msg = "toolhead get_position X:%s, Y:%s, Z:%s, E:%s" % (position[0], position[1], position[2], position[3])
|
||||
gcmd.respond_info(msg)
|
||||
def load_config(config):
|
||||
return GCodeMove(config)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,488 @@
|
||||
# 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(
|
||||
"""{"code":"key340", "msg":"Heaters %s Requested temperature (%.1f) out of range (%.1f:%.1f)", "values":["%s", %.1f, %.1f, %.1f]}"""
|
||||
% (self.name, degrees, self.min_temp, self.max_temp, self.name, 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
|
||||
self.long_temp =False
|
||||
self.old_temp = 0.0
|
||||
self.cnt_temp = 0
|
||||
self.prev_temp = AMBIENT_TEMP
|
||||
self.temp_coff = 1.
|
||||
self.diff_tempa = 0
|
||||
self.diff_tempb = 0
|
||||
def temperature_update(self, read_time, temp, target_temp):
|
||||
if (temp + 5.0) < target_temp:
|
||||
self.long_temp = True
|
||||
self.old_temp = 0.0
|
||||
self.cnt_temp = 0
|
||||
if target_temp >= 20 and target_temp<=120:
|
||||
if temp + 0.7 > target_temp:
|
||||
self.long_temp =False
|
||||
if self.long_temp:
|
||||
if self.old_temp <= 0.01 or self.old_temp < temp:
|
||||
self.old_temp = temp
|
||||
self.cnt_temp = 0
|
||||
# self.diff_tempa = 16.1 + (119-16.1)/100.*(target_temp-20.0)
|
||||
# self.diff_tempb = 16.3 + (119.5-16.3)/100.*(target_temp-20.0)
|
||||
self.diff_tempa = 16.1 + 1.029 * (target_temp-20.0)
|
||||
self.diff_tempb = 16.3 + 1.032 * (target_temp-20.0)
|
||||
elif self.old_temp > temp:
|
||||
self.cnt_temp = self.cnt_temp + 1
|
||||
if self.cnt_temp > 10:
|
||||
self.long_temp =False
|
||||
else:
|
||||
# self.diff_tempa = 19.1 + (119.7-19.1)/100.*(target_temp-20.0)
|
||||
# self.diff_tempb = 19.3 + (120.2-19.3)/100.*(target_temp-20.0)
|
||||
self.diff_tempa = 19.1 + 1.006 * (target_temp-20.0)
|
||||
self.diff_tempb = 19.3 + 1.009 * (target_temp-20.0)
|
||||
if self.heating and temp >= self.diff_tempb:
|
||||
self.heating = False
|
||||
elif not self.heating and temp <= self.diff_tempa:
|
||||
self.heating = True
|
||||
else:
|
||||
if self.heating and temp >= target_temp:
|
||||
self.heating = False
|
||||
elif not self.heating and temp <= target_temp-self.max_delta:
|
||||
self.heating = True
|
||||
if self.heating:
|
||||
if self.prev_temp > 0.1:
|
||||
if self.prev_temp - target_temp > 3.:
|
||||
self.temp_coff = 0.3 * self.temp_coff
|
||||
elif self.prev_temp - target_temp > 2.:
|
||||
self.temp_coff = 0.5 *self.temp_coff
|
||||
elif self.prev_temp - target_temp > 1.5:
|
||||
self.temp_coff = 0.65 * self.temp_coff
|
||||
elif self.prev_temp - target_temp > 1.:
|
||||
self.temp_coff = 0.8 * self.temp_coff
|
||||
elif self.prev_temp < target_temp:
|
||||
self.temp_coff = 1.5 * self.temp_coff
|
||||
if (temp + 1.5) < target_temp:
|
||||
self.temp_coff = 1.0
|
||||
if self.temp_coff < 0.3:
|
||||
self.temp_coff = 0.3
|
||||
elif self.temp_coff > 1.0:
|
||||
self.temp_coff = 1.0
|
||||
self.prev_temp = 0.
|
||||
self.heater.set_pwm(read_time, self.heater_max_power * self.temp_coff)
|
||||
else:
|
||||
self.heater.set_pwm(read_time, 0.)
|
||||
if target_temp > 0.1:
|
||||
if self.prev_temp < temp:
|
||||
self.prev_temp = temp
|
||||
else:
|
||||
self.prev_temp = 0.
|
||||
self.temp_coff = 1.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 = 2.
|
||||
PID_SETTLE_SLOPE = .5
|
||||
|
||||
class ControlPID:
|
||||
def __init__(self, heater, config):
|
||||
self.printer = config.get_printer()
|
||||
self.oldco = 0
|
||||
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.powerpin = self.printer.lookup_object("power_pin")
|
||||
|
||||
# bounded_co = max(0., min(self.heater_max_power, co))
|
||||
# if bounded_co == self.heater_max_power:
|
||||
# if self.oldco == 0:
|
||||
# # self.powerpin.set_power_pin(0)
|
||||
# self.oldco = self.heater_max_power
|
||||
# else:
|
||||
# if self.oldco == self.heater_max_power:
|
||||
# # self.powerpin.set_power_pin(1)
|
||||
#
|
||||
# self.oldco = 0
|
||||
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)
|
||||
# Register webhooks
|
||||
webhooks = self.printer.lookup_object('webhooks')
|
||||
webhooks.register_endpoint("breakheater", self._handle_breakheater)
|
||||
self.can_break=False
|
||||
self.can_break_flag = 0
|
||||
self.extruder_temperature_wait = False
|
||||
self.bed_temperature_wait = False
|
||||
def _handle_breakheater(self,web_request):
|
||||
reactor = self.printer.get_reactor()
|
||||
for heater in self.heaters.values():
|
||||
eventtime = reactor.monotonic()
|
||||
if heater.check_busy(eventtime):
|
||||
self.can_break = True
|
||||
|
||||
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,
|
||||
'extruder_temperature_wait': self.extruder_temperature_wait,
|
||||
'bed_temperature_wait': self.bed_temperature_wait}
|
||||
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()
|
||||
self.can_break_flag = 1
|
||||
self.can_break = False
|
||||
if "heater_bed" in heater.name:
|
||||
self.bed_temperature_wait = True
|
||||
else:
|
||||
self.extruder_temperature_wait = True
|
||||
while not self.printer.is_shutdown() and heater.check_busy(eventtime) :
|
||||
if self.can_break:
|
||||
self.can_break_flag = 2
|
||||
self.can_break = False
|
||||
# toolhead._handle_shutdown()
|
||||
#toolhead.move_queue.reset()
|
||||
# self.turn_off_all_heaters()
|
||||
#gcode.run_script("G28")
|
||||
|
||||
break
|
||||
print_time = toolhead.get_last_move_time()
|
||||
gcode.respond_raw(self._get_temp(eventtime))
|
||||
eventtime = reactor.pause(eventtime + 1.)
|
||||
if self.can_break_flag != 2:
|
||||
self.can_break_flag = 3
|
||||
if "heater_bed" in heater.name:
|
||||
self.bed_temperature_wait = False
|
||||
else:
|
||||
self.extruder_temperature_wait = False
|
||||
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() and not self.can_break:
|
||||
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)
|
||||
@@ -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 = """{"code":"key20", "msg":"Error during homing move: %s", "values": [%s]}""" % (str(e),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 = """{"code":"key21", "msg":"Communication timeout during homing %s", "values": ["%s"]}""" % (name, name)
|
||||
elif check_triggered and error is None:
|
||||
error = """{"code":"key22", "msg":"No trigger on %s after full movement", "values": ["%s"]}""" % (name, 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(
|
||||
"""{"code":"key23", "msg":"Endstop %s still triggered after retract", "values": ["%s"]}"""
|
||||
% (hmove.check_no_movement(), 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(
|
||||
'{"code": "key4", "msg": "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(
|
||||
'{"code": "key5", "msg": "Probing failed due to printer shutdown"}')
|
||||
raise
|
||||
if hmove.check_no_movement() is not None:
|
||||
raise self.printer.command_error(
|
||||
'{"code": "key6", "msg": "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)
|
||||
@@ -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(
|
||||
"""{"code":"key68", "msg": "One or more of these heaters are unknown: %s", "values": ["%s"]}"""
|
||||
% (self.disable_heaters,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(
|
||||
"""{"code":"key67", "msg":"One or more of these steppers are unknown: %s", "values": ["%s"]}"""
|
||||
% (self.flaky_steppers, 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)
|
||||
@@ -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)
|
||||
@@ -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("""{"code":"key275": "msg":"Invalid HTU21D Resolution. Valid are %s", "values":["%s"]}"""
|
||||
% ('|'.join(HTU21D_RESOLUTIONS.keys()), '|'.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(
|
||||
"""{"code":"key195", "msg": "HTU21D temperature %0.1f outside range of %0.1f:%.01f", "values": [%0.1f, %.01f, %.01f]}"""
|
||||
% (self.temp, self.min_temp, self.max_temp, 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)
|
||||
@@ -0,0 +1,207 @@
|
||||
# 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.
|
||||
from os import remove
|
||||
import time
|
||||
import mcu
|
||||
import math
|
||||
|
||||
class HX711S:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object("gcode")
|
||||
self.s_count = config.getint('count', 1, 1, 4)
|
||||
self.base_avgs = [0, 0, 0, 0]
|
||||
self.del_dirty = False
|
||||
self.index_dirty = 0
|
||||
self.start_tick = 0
|
||||
self.need_wait = False
|
||||
self.s_clk_pin = []
|
||||
self.s_sdo_pin = []
|
||||
self.all_params = []
|
||||
self.all_vals = [[], [], [], []]
|
||||
for i in range(self.s_count):
|
||||
self.s_clk_pin.append(config.get('sensor%d_clk_pin' % i, None if i == 0 else self.s_clk_pin[i - 1]))
|
||||
self.s_sdo_pin.append(config.get('sensor%d_sdo_pin' % i, None if i == 0 else self.s_sdo_pin[i - 1]))
|
||||
self.mcu = mcu.get_printer_mcu(self.printer, config.get('use_mcu'))
|
||||
self.oid = self.mcu.create_oid()
|
||||
self.mcu.register_config_callback(self._build_config)
|
||||
self.mcu.register_response(self._handle_debug_hx711s, "debug_hx711s", self.oid)
|
||||
self.mcu.register_response(self._handle_result_hx711s, "result_hx711s", self.oid)
|
||||
self.printer.register_event_handler('klippy:mcu_identify', self._handle_mcu_identify)
|
||||
self.printer.register_event_handler("klippy:shutdown", self._handle_shutdown)
|
||||
self.printer.register_event_handler("klippy:disconnect", self._handle_disconnect)
|
||||
self.gcode.register_command('READ_HX711', self.cmd_READ_HX711, desc=self.cmd_READ_HX711_help)
|
||||
self.pi_count = int(0)
|
||||
self.show_msg = False
|
||||
self.filter = None
|
||||
self.query_cmd = None
|
||||
self.mcu_freq = 72000000
|
||||
self.last_send_heart = 0.
|
||||
self.is_shutdown = True
|
||||
self.is_timeout = True
|
||||
pass
|
||||
|
||||
def _build_config(self):
|
||||
self.mcu.add_config_cmd("config_hx711s oid=%d hx711_count=%d" % (self.oid, self.s_count))
|
||||
pins = self.printer.lookup_object("pins")
|
||||
for i in range(self.s_count):
|
||||
clk_pin_params = pins.lookup_pin(self.s_clk_pin[i])
|
||||
sdo_pin_params = pins.lookup_pin(self.s_sdo_pin[i])
|
||||
self.mcu.add_config_cmd("add_hx711s oid=%d index=%d clk_pin=%s sdo_pin=%s" % (self.oid, i, clk_pin_params['pin'], sdo_pin_params['pin']))
|
||||
# self.query_cmd = self.mcu.lookup_command("query_hx711s oid=%c times_read=%hu is_ck_con=%c", cq=None)
|
||||
self.query_cmd = self.mcu.lookup_command("query_hx711s oid=%c times_read=%hu", cq=None)
|
||||
self.filter = self.printer.lookup_object('filter')
|
||||
self.mcu_freq = self.mcu.get_constant_float('CLOCK_FREQ')
|
||||
pass
|
||||
|
||||
def _handle_mcu_identify(self):
|
||||
# self.send_heart_beat_cmd = self.mcu.lookup_query_command(
|
||||
# "heart_beat_hx711s oid=%c",
|
||||
# "heart_beat_hx711s_result oid=%c",
|
||||
# oid=self.oid, cq=None)
|
||||
pass
|
||||
|
||||
self.is_shutdown = False
|
||||
self.is_timeout = False
|
||||
pass
|
||||
|
||||
def _handle_debug_hx711s(self, params):
|
||||
self.printer.lookup_object('prtouch').pnt_msg(str(params))
|
||||
pass
|
||||
|
||||
def _handle_shutdown(self):
|
||||
self.is_shutdown = True
|
||||
pass
|
||||
|
||||
def _handle_disconnect(self):
|
||||
self.is_timeout = True
|
||||
pass
|
||||
|
||||
def _handle_result_hx711s(self, params):
|
||||
while self.need_wait:
|
||||
self.delay_s(0.001)
|
||||
self.start_tick = self.start_tick if len(self.all_params) != 0 else params['nt']
|
||||
if self.del_dirty and (params['vd'] != 0 or params['it'] > 20) and self.index_dirty == 0:
|
||||
self.index_dirty = 1
|
||||
return
|
||||
self.index_dirty -= 1 if self.index_dirty == 1 else 0
|
||||
self.all_params.append(params)
|
||||
for i in range(self.s_count):
|
||||
self.all_vals[i].append(params['v%d' % i] - self.base_avgs[i])
|
||||
if self.show_msg:
|
||||
self.gcode.respond_info('Hx711 Val=' + str(params))
|
||||
if len(self.all_params) > self.pi_count:
|
||||
del self.all_params[0]
|
||||
for i in range(self.s_count):
|
||||
del self.all_vals[i][0]
|
||||
pass
|
||||
|
||||
def query_start(self, pi_count, cycle_count, del_dirty=False, show_msg=False, is_ck_con=False):
|
||||
if self.is_shutdown or self.is_timeout:
|
||||
pass
|
||||
if cycle_count != 0:
|
||||
self.pi_count = pi_count
|
||||
self.all_params = []
|
||||
self.all_vals = [[], [], [], []]
|
||||
self.show_msg = show_msg
|
||||
self.del_dirty = del_dirty
|
||||
self.index_dirty = 0
|
||||
# self.query_cmd.send([self.oid, cycle_count, 1 if is_ck_con else 0])
|
||||
self.query_cmd.send([self.oid, cycle_count])
|
||||
pass
|
||||
|
||||
def get_params(self):
|
||||
self.need_wait = True
|
||||
tmps = [x for x in self.all_params]
|
||||
self.need_wait = False
|
||||
return tmps, self.start_tick
|
||||
|
||||
def get_vals(self):
|
||||
self.need_wait = True
|
||||
tmps = [[], [], [], []]
|
||||
for i in range(self.s_count):
|
||||
tmps[i] = [x for x in self.all_vals[i]]
|
||||
self.need_wait = False
|
||||
return tmps
|
||||
|
||||
def delay_s(self, delay_s):
|
||||
toolhead = self.printer.lookup_object("toolhead")
|
||||
reactor = self.printer.get_reactor()
|
||||
eventtime = reactor.monotonic()
|
||||
if not self.printer.is_shutdown():
|
||||
toolhead.get_last_move_time()
|
||||
eventtime = reactor.pause(eventtime + delay_s)
|
||||
pass
|
||||
|
||||
def send_heart_beat(self):
|
||||
# if time.time() - self.last_send_heart > 0.1:
|
||||
# self.send_heart_beat_cmd.send([self.oid])
|
||||
# self.last_send_heart = time.time()
|
||||
pass
|
||||
|
||||
def read_base(self, cnt, max_hold, reset_zero=True):
|
||||
avgs = [0, 0, 0, 0]
|
||||
rvs = [[], [], [], []]
|
||||
for i in range(3):
|
||||
self.base_avgs = [0, 0, 0, 0]
|
||||
avgs = [0, 0, 0, 0]
|
||||
self.query_start(cnt, cnt + 5, del_dirty=True, show_msg=False)
|
||||
t_last = time.time()
|
||||
while not (self.is_shutdown or self.is_timeout) and len(self.get_vals()[0]) < cnt and (time.time() - t_last) < cnt * 0.010 * 15:
|
||||
self.delay_s(0.010)
|
||||
pass
|
||||
vals = self.get_vals()
|
||||
if len(vals[0]) < cnt:
|
||||
raise self.printer.command_error("""{"code":"key503", "msg":"z-Touch::read_base: Can not read z-Touch data."}""")
|
||||
|
||||
for j in range(self.s_count):
|
||||
del vals[j][0:int(len(vals[j]) / 2)]
|
||||
for j in range(self.s_count):
|
||||
del vals[j][vals[j].index(min(vals[j]))]
|
||||
del vals[j][vals[j].index(min(vals[j]))]
|
||||
del vals[j][vals[j].index(max(vals[j]))]
|
||||
del vals[j][vals[j].index(max(vals[j]))]
|
||||
rvs = [[], [], [], []]
|
||||
tf = self.filter.get_tft()
|
||||
lf = self.filter.get_lft(0.5)
|
||||
for j in range(self.s_count):
|
||||
vals[j] = tf.ftr_val(vals[j])
|
||||
vals[j] = lf.ftr_val(vals[j])
|
||||
rvs[j].append(min(vals[j]))
|
||||
rvs[j].append(sum(vals[j]) / len(vals[j]))
|
||||
rvs[j].append(max(vals[j]))
|
||||
avgs[j] = sum(vals[j]) / len(vals[j])
|
||||
self.printer.lookup_object('prtouch').pnt_msg('READ_BASE ch=%d min=%.2f avg=%.2f max=%.2f' % (j, rvs[j][-3], avgs[j], rvs[j][-1]))
|
||||
if reset_zero:
|
||||
self.base_avgs = avgs
|
||||
|
||||
sum_max = 0
|
||||
for j in range(self.s_count):
|
||||
sum_max += math.fabs(rvs[j][2] - rvs[j][0])
|
||||
if sum_max < max_hold * 2:
|
||||
break
|
||||
return avgs, rvs
|
||||
|
||||
cmd_READ_HX711_help = "Read hx711s vals"
|
||||
|
||||
def cmd_READ_HX711(self, gcmd):
|
||||
cnt = gcmd.get_int('C', 1, minval=1, maxval=9999)
|
||||
self.query_start(cnt, cnt, False, False, False)
|
||||
self.delay_s(1.)
|
||||
self.base_avgs = [0, 0, 0, 0]
|
||||
vals = self.get_vals()
|
||||
for i in range(self.s_count):
|
||||
self.gcode.respond_info('CH%d=' % i)
|
||||
sv = '['
|
||||
for j in range(len(vals[i])):
|
||||
sv += '%.2f, ' % vals[i][j]
|
||||
self.gcode.respond_info(sv + ']')
|
||||
self.read_base(40, 500000)
|
||||
pass
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return HX711S(config)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,171 @@
|
||||
# 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(
|
||||
"""{"code":"key24", "msg":"Unsupported shaper type: %s", "values": ["%s"]}""" % (
|
||||
self.shaper_type, 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("""{"code":"key24", "msg":"Unsupported shaper type: %s", "values": ["%s"]}""" % (
|
||||
shaper_type, 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)
|
||||
gcode.register_command("UPDATE_INPUT_SHAPER",
|
||||
self.cmd_UPDATE_INPUT_SHAPER,
|
||||
desc=self.cmd_UPDATE_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("""{"code":"key25", "msg":"Failed to configure shaper(s) %s with given parameters", "values": ["%s"]}"""
|
||||
% (', '.join([s.get_name() for s in failed]), ', '.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)
|
||||
cmd_UPDATE_INPUT_SHAPER_help = "cmd_UPDATE_INPUT_SHAPER parameters for input shaper"
|
||||
def cmd_UPDATE_INPUT_SHAPER(self, gcmd):
|
||||
self.connect()
|
||||
|
||||
def load_config(config):
|
||||
return InputShaper(config)
|
||||
@@ -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(
|
||||
"""{"code":"key196", "msg": "LM75 temperature %0.1f outside range of %0.1f:%.01f", "values": [%0.1f,%0.1f,%0.1f]}"""
|
||||
% (self.temp, self.min_temp, self.max_temp, 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)
|
||||
@@ -0,0 +1,262 @@
|
||||
# Helper script for manual z height probing
|
||||
#
|
||||
# Copyright (C) 2019 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging, bisect
|
||||
|
||||
class ManualProbe:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
# Register commands
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.gcode_move = self.printer.load_object(config, "gcode_move")
|
||||
self.gcode.register_command('MANUAL_PROBE', self.cmd_MANUAL_PROBE,
|
||||
desc=self.cmd_MANUAL_PROBE_help)
|
||||
# Endstop value for cartesian printers with separate Z axis
|
||||
zconfig = config.getsection('stepper_z')
|
||||
self.z_position_endstop = zconfig.getfloat('position_endstop', None,
|
||||
note_valid=False)
|
||||
# Endstop values for linear delta printers with vertical A,B,C towers
|
||||
a_tower_config = config.getsection('stepper_a')
|
||||
self.a_position_endstop = a_tower_config.getfloat('position_endstop',
|
||||
None,
|
||||
note_valid=False)
|
||||
b_tower_config = config.getsection('stepper_b')
|
||||
self.b_position_endstop = b_tower_config.getfloat('position_endstop',
|
||||
None,
|
||||
note_valid=False)
|
||||
c_tower_config = config.getsection('stepper_c')
|
||||
self.c_position_endstop = c_tower_config.getfloat('position_endstop',
|
||||
None,
|
||||
note_valid=False)
|
||||
# Conditionally register appropriate commands depending on printer
|
||||
# Cartestian printers with separate Z Axis
|
||||
if self.z_position_endstop is not None:
|
||||
self.gcode.register_command(
|
||||
'Z_ENDSTOP_CALIBRATE', self.cmd_Z_ENDSTOP_CALIBRATE,
|
||||
desc=self.cmd_Z_ENDSTOP_CALIBRATE_help)
|
||||
self.gcode.register_command(
|
||||
'Z_OFFSET_APPLY_ENDSTOP',
|
||||
self.cmd_Z_OFFSET_APPLY_ENDSTOP,
|
||||
desc=self.cmd_Z_OFFSET_APPLY_ENDSTOP_help)
|
||||
# Linear delta printers with A,B,C towers
|
||||
if 'delta' == config.getsection('printer').get('kinematics'):
|
||||
self.gcode.register_command(
|
||||
'Z_OFFSET_APPLY_ENDSTOP',
|
||||
self.cmd_Z_OFFSET_APPLY_DELTA_ENDSTOPS,
|
||||
desc=self.cmd_Z_OFFSET_APPLY_ENDSTOP_help)
|
||||
self.reset_status()
|
||||
def manual_probe_finalize(self, kin_pos):
|
||||
if kin_pos is not None:
|
||||
self.gcode.respond_info("Z position is %.3f" % (kin_pos[2],))
|
||||
def reset_status(self):
|
||||
self.status = {
|
||||
'is_active': False,
|
||||
'z_position': None,
|
||||
'z_position_lower': None,
|
||||
'z_position_upper': None
|
||||
}
|
||||
def get_status(self, eventtime):
|
||||
return self.status
|
||||
cmd_MANUAL_PROBE_help = "Start manual probe helper script"
|
||||
def cmd_MANUAL_PROBE(self, gcmd):
|
||||
ManualProbeHelper(self.printer, gcmd, self.manual_probe_finalize)
|
||||
def z_endstop_finalize(self, kin_pos):
|
||||
if kin_pos is None:
|
||||
return
|
||||
z_pos = self.z_position_endstop - kin_pos[2]
|
||||
self.gcode.respond_info(
|
||||
"stepper_z: position_endstop: %.3f\n"
|
||||
"The SAVE_CONFIG command will update the printer config file\n"
|
||||
"with the above and restart the printer." % (z_pos,))
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
configfile.set('stepper_z', 'position_endstop', "%.3f" % (z_pos,))
|
||||
cmd_Z_ENDSTOP_CALIBRATE_help = "Calibrate a Z endstop"
|
||||
def cmd_Z_ENDSTOP_CALIBRATE(self, gcmd):
|
||||
ManualProbeHelper(self.printer, gcmd, self.z_endstop_finalize)
|
||||
def cmd_Z_OFFSET_APPLY_ENDSTOP(self,gcmd):
|
||||
offset = self.gcode_move.get_status()['homing_origin'].z
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
if offset == 0:
|
||||
self.gcode.respond_info("Nothing to do: Z Offset is 0")
|
||||
else:
|
||||
new_calibrate = self.z_position_endstop - offset
|
||||
self.gcode.respond_info(
|
||||
"stepper_z: position_endstop: %.3f\n"
|
||||
"The SAVE_CONFIG command will update the printer config file\n"
|
||||
"with the above and restart the printer." % (new_calibrate))
|
||||
configfile.set('stepper_z', 'position_endstop',
|
||||
"%.3f" % (new_calibrate,))
|
||||
def cmd_Z_OFFSET_APPLY_DELTA_ENDSTOPS(self,gcmd):
|
||||
offset = self.gcode_move.get_status()['homing_origin'].z
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
if offset == 0:
|
||||
self.gcode.respond_info("Nothing to do: Z Offset is 0")
|
||||
else:
|
||||
new_a_calibrate = self.a_position_endstop - offset
|
||||
new_b_calibrate = self.b_position_endstop - offset
|
||||
new_c_calibrate = self.c_position_endstop - offset
|
||||
self.gcode.respond_info(
|
||||
"stepper_a: position_endstop: %.3f\n"
|
||||
"stepper_b: position_endstop: %.3f\n"
|
||||
"stepper_c: position_endstop: %.3f\n"
|
||||
"The SAVE_CONFIG command will update the printer config file\n"
|
||||
"with the above and restart the printer." % (new_a_calibrate,
|
||||
new_b_calibrate,
|
||||
new_c_calibrate))
|
||||
configfile.set('stepper_a', 'position_endstop',
|
||||
"%.3f" % (new_a_calibrate,))
|
||||
configfile.set('stepper_b', 'position_endstop',
|
||||
"%.3f" % (new_b_calibrate,))
|
||||
configfile.set('stepper_c', 'position_endstop',
|
||||
"%.3f" % (new_c_calibrate,))
|
||||
cmd_Z_OFFSET_APPLY_ENDSTOP_help = "Adjust the z endstop_position"
|
||||
|
||||
# Verify that a manual probe isn't already in progress
|
||||
def verify_no_manual_probe(printer):
|
||||
gcode = printer.lookup_object('gcode')
|
||||
try:
|
||||
gcode.register_command('ACCEPT', 'dummy')
|
||||
except printer.config_error as e:
|
||||
raise gcode.error(
|
||||
"Already in a manual Z probe. Use ABORT to abort it.")
|
||||
gcode.register_command('ACCEPT', None)
|
||||
|
||||
Z_BOB_MINIMUM = 0.500
|
||||
BISECT_MAX = 0.200
|
||||
|
||||
# Helper script to determine a Z height
|
||||
class ManualProbeHelper:
|
||||
def __init__(self, printer, gcmd, finalize_callback):
|
||||
self.printer = printer
|
||||
self.finalize_callback = finalize_callback
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.toolhead = self.printer.lookup_object('toolhead')
|
||||
self.manual_probe = self.printer.lookup_object('manual_probe')
|
||||
self.speed = gcmd.get_float("SPEED", 5.)
|
||||
self.past_positions = []
|
||||
self.last_toolhead_pos = self.last_kinematics_pos = None
|
||||
# Register commands
|
||||
verify_no_manual_probe(printer)
|
||||
self.gcode.register_command('ACCEPT', self.cmd_ACCEPT,
|
||||
desc=self.cmd_ACCEPT_help)
|
||||
self.gcode.register_command('NEXT', self.cmd_ACCEPT)
|
||||
self.gcode.register_command('ABORT', self.cmd_ABORT,
|
||||
desc=self.cmd_ABORT_help)
|
||||
self.gcode.register_command('TESTZ', self.cmd_TESTZ,
|
||||
desc=self.cmd_TESTZ_help)
|
||||
self.gcode.respond_info(
|
||||
"Starting manual Z probe. Use TESTZ to adjust position.\n"
|
||||
"Finish with ACCEPT or ABORT command.")
|
||||
self.start_position = self.toolhead.get_position()
|
||||
self.report_z_status()
|
||||
def get_kinematics_pos(self):
|
||||
toolhead_pos = self.toolhead.get_position()
|
||||
if toolhead_pos == self.last_toolhead_pos:
|
||||
return self.last_kinematics_pos
|
||||
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()}
|
||||
kin_pos = kin.calc_position(kin_spos)
|
||||
self.last_toolhead_pos = toolhead_pos
|
||||
self.last_kinematics_pos = kin_pos
|
||||
return kin_pos
|
||||
def move_z(self, z_pos):
|
||||
curpos = self.toolhead.get_position()
|
||||
try:
|
||||
z_bob_pos = z_pos + Z_BOB_MINIMUM
|
||||
if curpos[2] < z_bob_pos:
|
||||
self.toolhead.manual_move([None, None, z_bob_pos], self.speed)
|
||||
self.toolhead.manual_move([None, None, z_pos], self.speed)
|
||||
except self.printer.command_error as e:
|
||||
self.finalize(False)
|
||||
raise
|
||||
def report_z_status(self, warn_no_change=False, prev_pos=None):
|
||||
# Get position
|
||||
kin_pos = self.get_kinematics_pos()
|
||||
z_pos = kin_pos[2]
|
||||
if warn_no_change and z_pos == prev_pos:
|
||||
self.gcode.respond_info(
|
||||
"WARNING: No change in position (reached stepper resolution)")
|
||||
# Find recent positions that were tested
|
||||
pp = self.past_positions
|
||||
next_pos = bisect.bisect_left(pp, z_pos)
|
||||
prev_pos = next_pos - 1
|
||||
if next_pos < len(pp) and pp[next_pos] == z_pos:
|
||||
next_pos += 1
|
||||
prev_pos_val = next_pos_val = None
|
||||
prev_str = next_str = "??????"
|
||||
if prev_pos >= 0:
|
||||
prev_pos_val = pp[prev_pos]
|
||||
prev_str = "%.3f" % (prev_pos_val,)
|
||||
if next_pos < len(pp):
|
||||
next_pos_val = pp[next_pos]
|
||||
next_str = "%.3f" % (next_pos_val,)
|
||||
self.manual_probe.status = {
|
||||
'is_active': True,
|
||||
'z_position': z_pos,
|
||||
'z_position_lower': prev_pos_val,
|
||||
'z_position_upper': next_pos_val,
|
||||
}
|
||||
# Find recent positions
|
||||
self.gcode.respond_info("Z position: %s --> %.3f <-- %s"
|
||||
% (prev_str, z_pos, next_str))
|
||||
cmd_ACCEPT_help = "Accept the current Z position"
|
||||
def cmd_ACCEPT(self, gcmd):
|
||||
pos = self.toolhead.get_position()
|
||||
start_pos = self.start_position
|
||||
if pos[:2] != start_pos[:2] or pos[2] >= start_pos[2]:
|
||||
gcmd.respond_info(
|
||||
"Manual probe failed! Use TESTZ commands to position the\n"
|
||||
"nozzle prior to running ACCEPT.")
|
||||
self.finalize(False)
|
||||
return
|
||||
self.finalize(True)
|
||||
cmd_ABORT_help = "Abort manual Z probing tool"
|
||||
def cmd_ABORT(self, gcmd):
|
||||
self.finalize(False)
|
||||
cmd_TESTZ_help = "Move to new Z height"
|
||||
def cmd_TESTZ(self, gcmd):
|
||||
# Store current position for later reference
|
||||
kin_pos = self.get_kinematics_pos()
|
||||
z_pos = kin_pos[2]
|
||||
pp = self.past_positions
|
||||
insert_pos = bisect.bisect_left(pp, z_pos)
|
||||
if insert_pos >= len(pp) or pp[insert_pos] != z_pos:
|
||||
pp.insert(insert_pos, z_pos)
|
||||
# Determine next position to move to
|
||||
req = gcmd.get("Z")
|
||||
if req in ('+', '++'):
|
||||
check_z = 9999999999999.9
|
||||
if insert_pos < len(self.past_positions) - 1:
|
||||
check_z = self.past_positions[insert_pos + 1]
|
||||
if req == '+':
|
||||
check_z = (check_z + z_pos) / 2.
|
||||
next_z_pos = min(check_z, z_pos + BISECT_MAX)
|
||||
elif req in ('-', '--'):
|
||||
check_z = -9999999999999.9
|
||||
if insert_pos > 0:
|
||||
check_z = self.past_positions[insert_pos - 1]
|
||||
if req == '-':
|
||||
check_z = (check_z + z_pos) / 2.
|
||||
next_z_pos = max(check_z, z_pos - BISECT_MAX)
|
||||
else:
|
||||
next_z_pos = z_pos + gcmd.get_float("Z")
|
||||
# Move to given position and report it
|
||||
self.move_z(next_z_pos)
|
||||
self.report_z_status(next_z_pos != z_pos, z_pos)
|
||||
def finalize(self, success):
|
||||
self.manual_probe.reset_status()
|
||||
self.gcode.register_command('ACCEPT', None)
|
||||
self.gcode.register_command('NEXT', None)
|
||||
self.gcode.register_command('ABORT', None)
|
||||
self.gcode.register_command('TESTZ', None)
|
||||
kin_pos = None
|
||||
if success:
|
||||
kin_pos = self.get_kinematics_pos()
|
||||
self.finalize_callback(kin_pos)
|
||||
|
||||
def load_config(config):
|
||||
return ManualProbe(config)
|
||||
@@ -0,0 +1,128 @@
|
||||
# Support for a manual controlled stepper
|
||||
#
|
||||
# Copyright (C) 2019-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import stepper, chelper
|
||||
from . import force_move
|
||||
|
||||
class ManualStepper:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
if config.get('endstop_pin', None) is not None:
|
||||
self.can_home = True
|
||||
self.rail = stepper.PrinterRail(
|
||||
config, need_position_minmax=False, default_position_endstop=0.)
|
||||
self.steppers = self.rail.get_steppers()
|
||||
else:
|
||||
self.can_home = False
|
||||
self.rail = stepper.PrinterStepper(config)
|
||||
self.steppers = [self.rail]
|
||||
self.velocity = config.getfloat('velocity', 5., above=0.)
|
||||
self.accel = self.homing_accel = config.getfloat('accel', 0., minval=0.)
|
||||
self.next_cmd_time = 0.
|
||||
# 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.rail.setup_itersolve('cartesian_stepper_alloc', b'x')
|
||||
self.rail.set_trapq(self.trapq)
|
||||
# Register commands
|
||||
stepper_name = config.get_name().split()[1]
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_mux_command('MANUAL_STEPPER', "STEPPER",
|
||||
stepper_name, self.cmd_MANUAL_STEPPER,
|
||||
desc=self.cmd_MANUAL_STEPPER_help)
|
||||
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 do_enable(self, enable):
|
||||
self.sync_print_time()
|
||||
stepper_enable = self.printer.lookup_object('stepper_enable')
|
||||
if enable:
|
||||
for s in self.steppers:
|
||||
se = stepper_enable.lookup_enable(s.get_name())
|
||||
se.motor_enable(self.next_cmd_time)
|
||||
else:
|
||||
for s in self.steppers:
|
||||
se = stepper_enable.lookup_enable(s.get_name())
|
||||
se.motor_disable(self.next_cmd_time)
|
||||
self.sync_print_time()
|
||||
def do_set_position(self, setpos):
|
||||
self.rail.set_position([setpos, 0., 0.])
|
||||
def do_move(self, movepos, speed, accel, sync=True):
|
||||
self.sync_print_time()
|
||||
cp = self.rail.get_commanded_position()
|
||||
dist = movepos - cp
|
||||
axis_r, accel_t, cruise_t, cruise_v = force_move.calc_move_time(
|
||||
dist, speed, accel)
|
||||
self.trapq_append(self.trapq, self.next_cmd_time,
|
||||
accel_t, cruise_t, accel_t,
|
||||
cp, 0., 0., axis_r, 0., 0.,
|
||||
0., cruise_v, accel)
|
||||
self.next_cmd_time = self.next_cmd_time + accel_t + cruise_t + accel_t
|
||||
self.rail.generate_steps(self.next_cmd_time)
|
||||
self.trapq_finalize_moves(self.trapq, self.next_cmd_time + 99999.9)
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.note_kinematic_activity(self.next_cmd_time)
|
||||
if sync:
|
||||
self.sync_print_time()
|
||||
def do_homing_move(self, movepos, speed, accel, triggered, check_trigger):
|
||||
if not self.can_home:
|
||||
raise self.printer.command_error(
|
||||
"""{"code":"key198", "msg": "No endstop for this manual stepper", "values": []}""")
|
||||
self.homing_accel = accel
|
||||
pos = [movepos, 0., 0., 0.]
|
||||
endstops = self.rail.get_endstops()
|
||||
phoming = self.printer.lookup_object('homing')
|
||||
phoming.manual_home(self, endstops, pos, speed,
|
||||
triggered, check_trigger)
|
||||
cmd_MANUAL_STEPPER_help = "Command a manually configured stepper"
|
||||
def cmd_MANUAL_STEPPER(self, gcmd):
|
||||
enable = gcmd.get_int('ENABLE', None)
|
||||
if enable is not None:
|
||||
self.do_enable(enable)
|
||||
setpos = gcmd.get_float('SET_POSITION', None)
|
||||
if setpos is not None:
|
||||
self.do_set_position(setpos)
|
||||
speed = gcmd.get_float('SPEED', self.velocity, above=0.)
|
||||
accel = gcmd.get_float('ACCEL', self.accel, minval=0.)
|
||||
homing_move = gcmd.get_int('STOP_ON_ENDSTOP', 0)
|
||||
if homing_move:
|
||||
movepos = gcmd.get_float('MOVE')
|
||||
self.do_homing_move(movepos, speed, accel,
|
||||
homing_move > 0, abs(homing_move) == 1)
|
||||
elif gcmd.get_float('MOVE', None) is not None:
|
||||
movepos = gcmd.get_float('MOVE')
|
||||
sync = gcmd.get_int('SYNC', 1)
|
||||
self.do_move(movepos, speed, accel, sync)
|
||||
elif gcmd.get_int('SYNC', 0):
|
||||
self.sync_print_time()
|
||||
# Toolhead wrappers to support homing
|
||||
def flush_step_generation(self):
|
||||
self.sync_print_time()
|
||||
def get_position(self):
|
||||
return [self.rail.get_commanded_position(), 0., 0., 0.]
|
||||
def set_position(self, newpos, homing_axes=()):
|
||||
self.do_set_position(newpos[0])
|
||||
def get_last_move_time(self):
|
||||
self.sync_print_time()
|
||||
return self.next_cmd_time
|
||||
def dwell(self, delay):
|
||||
self.next_cmd_time += max(0., delay)
|
||||
def drip_move(self, newpos, speed, drip_completion):
|
||||
self.do_move(newpos[0], speed, self.homing_accel)
|
||||
def get_kinematics(self):
|
||||
return self
|
||||
def get_steppers(self):
|
||||
return self.steppers
|
||||
def calc_position(self, stepper_positions):
|
||||
return [stepper_positions[self.rail.get_name()], 0., 0.]
|
||||
|
||||
def load_config_prefix(config):
|
||||
return ManualStepper(config)
|
||||
Executable
+1196
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,333 @@
|
||||
# Diagnostic tool for reporting stepper and kinematic positions
|
||||
#
|
||||
# Copyright (C) 2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
import chelper
|
||||
|
||||
API_UPDATE_INTERVAL = 0.500
|
||||
|
||||
# Helper to periodically transmit data to a set of API clients
|
||||
class APIDumpHelper:
|
||||
def __init__(self, printer, data_cb, startstop_cb=None,
|
||||
update_interval=API_UPDATE_INTERVAL):
|
||||
self.printer = printer
|
||||
self.data_cb = data_cb
|
||||
if startstop_cb is None:
|
||||
startstop_cb = (lambda is_start: None)
|
||||
self.startstop_cb = startstop_cb
|
||||
self.is_started = False
|
||||
self.update_interval = update_interval
|
||||
self.update_timer = None
|
||||
self.clients = {}
|
||||
def _stop(self):
|
||||
self.clients.clear()
|
||||
reactor = self.printer.get_reactor()
|
||||
reactor.unregister_timer(self.update_timer)
|
||||
self.update_timer = None
|
||||
if not self.is_started:
|
||||
return reactor.NEVER
|
||||
try:
|
||||
self.startstop_cb(False)
|
||||
except self.printer.command_error as e:
|
||||
logging.exception("API Dump Helper stop callback error")
|
||||
self.clients.clear()
|
||||
self.is_started = False
|
||||
if self.clients:
|
||||
# New client started while in process of stopping
|
||||
self._start()
|
||||
return reactor.NEVER
|
||||
def _start(self):
|
||||
if self.is_started:
|
||||
return
|
||||
self.is_started = True
|
||||
try:
|
||||
self.startstop_cb(True)
|
||||
except self.printer.command_error as e:
|
||||
logging.exception("API Dump Helper start callback error")
|
||||
self.is_started = False
|
||||
self.clients.clear()
|
||||
raise
|
||||
reactor = self.printer.get_reactor()
|
||||
systime = reactor.monotonic()
|
||||
waketime = systime + self.update_interval
|
||||
self.update_timer = reactor.register_timer(self._update, waketime)
|
||||
def add_client(self, web_request):
|
||||
cconn = web_request.get_client_connection()
|
||||
template = web_request.get_dict('response_template', {})
|
||||
self.clients[cconn] = template
|
||||
self._start()
|
||||
def add_internal_client(self):
|
||||
cconn = InternalDumpClient()
|
||||
self.clients[cconn] = {}
|
||||
self._start()
|
||||
return cconn
|
||||
def _update(self, eventtime):
|
||||
try:
|
||||
msg = self.data_cb(eventtime)
|
||||
except self.printer.command_error as e:
|
||||
logging.exception("API Dump Helper data callback error")
|
||||
return self._stop()
|
||||
if not msg:
|
||||
return eventtime + self.update_interval
|
||||
for cconn, template in list(self.clients.items()):
|
||||
if cconn.is_closed():
|
||||
del self.clients[cconn]
|
||||
if not self.clients:
|
||||
return self._stop()
|
||||
continue
|
||||
tmp = dict(template)
|
||||
tmp['params'] = msg
|
||||
cconn.send(tmp)
|
||||
return eventtime + self.update_interval
|
||||
|
||||
# An "internal webhooks" wrapper for using APIDumpHelper internally
|
||||
class InternalDumpClient:
|
||||
def __init__(self):
|
||||
self.msgs = []
|
||||
self.is_done = False
|
||||
def get_messages(self):
|
||||
return self.msgs
|
||||
def finalize(self):
|
||||
self.is_done = True
|
||||
def is_closed(self):
|
||||
return self.is_done
|
||||
def send(self, msg):
|
||||
self.msgs.append(msg)
|
||||
if len(self.msgs) >= 10000:
|
||||
# Avoid filling up memory with too many samples
|
||||
self.finalize()
|
||||
|
||||
# Extract stepper queue_step messages
|
||||
class DumpStepper:
|
||||
def __init__(self, printer, mcu_stepper):
|
||||
self.printer = printer
|
||||
self.mcu_stepper = mcu_stepper
|
||||
self.last_api_clock = 0
|
||||
self.api_dump = APIDumpHelper(printer, self._api_update)
|
||||
wh = self.printer.lookup_object('webhooks')
|
||||
wh.register_mux_endpoint("motion_report/dump_stepper", "name",
|
||||
mcu_stepper.get_name(), self._add_api_client)
|
||||
def get_step_queue(self, start_clock, end_clock):
|
||||
mcu_stepper = self.mcu_stepper
|
||||
res = []
|
||||
while 1:
|
||||
data, count = mcu_stepper.dump_steps(128, start_clock, end_clock)
|
||||
if not count:
|
||||
break
|
||||
res.append((data, count))
|
||||
if count < len(data):
|
||||
break
|
||||
end_clock = data[count-1].first_clock
|
||||
res.reverse()
|
||||
return ([d[i] for d, cnt in res for i in range(cnt-1, -1, -1)], res)
|
||||
def log_steps(self, data):
|
||||
if not data:
|
||||
return
|
||||
out = []
|
||||
out.append("Dumping stepper '%s' (%s) %d queue_step:"
|
||||
% (self.mcu_stepper.get_name(),
|
||||
self.mcu_stepper.get_mcu().get_name(), len(data)))
|
||||
for i, s in enumerate(data):
|
||||
out.append("queue_step %d: t=%d p=%d i=%d c=%d a=%d"
|
||||
% (i, s.first_clock, s.start_position, s.interval,
|
||||
s.step_count, s.add))
|
||||
logging.info('\n'.join(out))
|
||||
def _api_update(self, eventtime):
|
||||
data, cdata = self.get_step_queue(self.last_api_clock, 1<<63)
|
||||
if not data:
|
||||
return {}
|
||||
clock_to_print_time = self.mcu_stepper.get_mcu().clock_to_print_time
|
||||
first = data[0]
|
||||
first_clock = first.first_clock
|
||||
first_time = clock_to_print_time(first_clock)
|
||||
self.last_api_clock = last_clock = data[-1].last_clock
|
||||
last_time = clock_to_print_time(last_clock)
|
||||
mcu_pos = first.start_position
|
||||
start_position = self.mcu_stepper.mcu_to_commanded_position(mcu_pos)
|
||||
step_dist = self.mcu_stepper.get_step_dist()
|
||||
if self.mcu_stepper.get_dir_inverted()[0]:
|
||||
step_dist = -step_dist
|
||||
d = [(s.interval, s.step_count, s.add) for s in data]
|
||||
return {"data": d, "start_position": start_position,
|
||||
"start_mcu_position": mcu_pos, "step_distance": step_dist,
|
||||
"first_clock": first_clock, "first_step_time": first_time,
|
||||
"last_clock": last_clock, "last_step_time": last_time}
|
||||
def _add_api_client(self, web_request):
|
||||
self.api_dump.add_client(web_request)
|
||||
hdr = ('interval', 'count', 'add')
|
||||
web_request.send({'header': hdr})
|
||||
|
||||
NEVER_TIME = 9999999999999999.
|
||||
|
||||
# Extract trapezoidal motion queue (trapq)
|
||||
class DumpTrapQ:
|
||||
def __init__(self, printer, name, trapq):
|
||||
self.printer = printer
|
||||
self.name = name
|
||||
self.trapq = trapq
|
||||
self.last_api_msg = (0., 0.)
|
||||
self.api_dump = APIDumpHelper(printer, self._api_update)
|
||||
wh = self.printer.lookup_object('webhooks')
|
||||
wh.register_mux_endpoint("motion_report/dump_trapq", "name", name,
|
||||
self._add_api_client)
|
||||
def extract_trapq(self, start_time, end_time):
|
||||
ffi_main, ffi_lib = chelper.get_ffi()
|
||||
res = []
|
||||
while 1:
|
||||
data = ffi_main.new('struct pull_move[128]')
|
||||
count = ffi_lib.trapq_extract_old(self.trapq, data, len(data),
|
||||
start_time, end_time)
|
||||
if not count:
|
||||
break
|
||||
res.append((data, count))
|
||||
if count < len(data):
|
||||
break
|
||||
end_time = data[count-1].print_time
|
||||
res.reverse()
|
||||
return ([d[i] for d, cnt in res for i in range(cnt-1, -1, -1)], res)
|
||||
def log_trapq(self, data):
|
||||
if not data:
|
||||
return
|
||||
out = ["Dumping trapq '%s' %d moves:" % (self.name, len(data))]
|
||||
for i, m in enumerate(data):
|
||||
out.append("move %d: pt=%.6f mt=%.6f sv=%.6f a=%.6f"
|
||||
" sp=(%.6f,%.6f,%.6f) ar=(%.6f,%.6f,%.6f)"
|
||||
% (i, m.print_time, m.move_t, m.start_v, m.accel,
|
||||
m.start_x, m.start_y, m.start_z, m.x_r, m.y_r, m.z_r))
|
||||
logging.info('\n'.join(out))
|
||||
def get_trapq_position(self, print_time):
|
||||
ffi_main, ffi_lib = chelper.get_ffi()
|
||||
data = ffi_main.new('struct pull_move[1]')
|
||||
count = ffi_lib.trapq_extract_old(self.trapq, data, 1, 0., print_time)
|
||||
if not count:
|
||||
return None, None
|
||||
move = data[0]
|
||||
move_time = max(0., min(move.move_t, print_time - move.print_time))
|
||||
dist = (move.start_v + .5 * move.accel * move_time) * move_time;
|
||||
pos = (move.start_x + move.x_r * dist, move.start_y + move.y_r * dist,
|
||||
move.start_z + move.z_r * dist)
|
||||
velocity = move.start_v + move.accel * move_time
|
||||
return pos, velocity
|
||||
def _api_update(self, eventtime):
|
||||
qtime = self.last_api_msg[0] + min(self.last_api_msg[1], 0.100)
|
||||
data, cdata = self.extract_trapq(qtime, NEVER_TIME)
|
||||
d = [(m.print_time, m.move_t, m.start_v, m.accel,
|
||||
(m.start_x, m.start_y, m.start_z), (m.x_r, m.y_r, m.z_r))
|
||||
for m in data]
|
||||
if d and d[0] == self.last_api_msg:
|
||||
d.pop(0)
|
||||
if not d:
|
||||
return {}
|
||||
self.last_api_msg = d[-1]
|
||||
return {"data": d}
|
||||
def _add_api_client(self, web_request):
|
||||
self.api_dump.add_client(web_request)
|
||||
hdr = ('time', 'duration', 'start_velocity', 'acceleration',
|
||||
'start_position', 'direction')
|
||||
web_request.send({'header': hdr})
|
||||
|
||||
STATUS_REFRESH_TIME = 0.250
|
||||
|
||||
class PrinterMotionReport:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.steppers = {}
|
||||
self.trapqs = {}
|
||||
# get_status information
|
||||
self.next_status_time = 0.
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
self.last_status = {
|
||||
'live_position': gcode.Coord(0., 0., 0., 0.),
|
||||
'live_velocity': 0., 'live_extruder_velocity': 0.,
|
||||
'steppers': [], 'trapq': [],
|
||||
}
|
||||
# Register handlers
|
||||
self.printer.register_event_handler("klippy:connect", self._connect)
|
||||
self.printer.register_event_handler("klippy:shutdown", self._shutdown)
|
||||
def register_stepper(self, config, mcu_stepper):
|
||||
ds = DumpStepper(self.printer, mcu_stepper)
|
||||
self.steppers[mcu_stepper.get_name()] = ds
|
||||
def _connect(self):
|
||||
# Lookup toolhead trapq
|
||||
toolhead = self.printer.lookup_object("toolhead")
|
||||
trapq = toolhead.get_trapq()
|
||||
self.trapqs['toolhead'] = DumpTrapQ(self.printer, 'toolhead', trapq)
|
||||
# Lookup extruder trapqs
|
||||
for i in range(99):
|
||||
ename = "extruder%d" % (i,)
|
||||
if ename == "extruder0":
|
||||
ename = "extruder"
|
||||
extruder = self.printer.lookup_object(ename, None)
|
||||
if extruder is None:
|
||||
break
|
||||
etrapq = extruder.get_trapq()
|
||||
self.trapqs[ename] = DumpTrapQ(self.printer, ename, etrapq)
|
||||
# Populate 'trapq' and 'steppers' in get_status result
|
||||
self.last_status['steppers'] = list(sorted(self.steppers.keys()))
|
||||
self.last_status['trapq'] = list(sorted(self.trapqs.keys()))
|
||||
# Shutdown handling
|
||||
def _dump_shutdown(self, eventtime):
|
||||
# Log stepper queue_steps on mcu that started shutdown (if any)
|
||||
shutdown_time = NEVER_TIME
|
||||
for dstepper in self.steppers.values():
|
||||
mcu = dstepper.mcu_stepper.get_mcu()
|
||||
sc = mcu.get_shutdown_clock()
|
||||
if not sc:
|
||||
continue
|
||||
shutdown_time = min(shutdown_time, mcu.clock_to_print_time(sc))
|
||||
clock_100ms = mcu.seconds_to_clock(0.100)
|
||||
start_clock = max(0, sc - clock_100ms)
|
||||
end_clock = sc + clock_100ms
|
||||
data, cdata = dstepper.get_step_queue(start_clock, end_clock)
|
||||
dstepper.log_steps(data)
|
||||
if shutdown_time >= NEVER_TIME:
|
||||
return
|
||||
# Log trapqs around time of shutdown
|
||||
for dtrapq in self.trapqs.values():
|
||||
data, cdata = dtrapq.extract_trapq(shutdown_time - .100,
|
||||
shutdown_time + .100)
|
||||
dtrapq.log_trapq(data)
|
||||
# Log estimated toolhead position at time of shutdown
|
||||
dtrapq = self.trapqs.get('toolhead')
|
||||
if dtrapq is None:
|
||||
return
|
||||
pos, velocity = dtrapq.get_trapq_position(shutdown_time)
|
||||
if pos is not None:
|
||||
logging.info("Requested toolhead position at shutdown time %.6f: %s"
|
||||
, shutdown_time, pos)
|
||||
def _shutdown(self):
|
||||
self.printer.get_reactor().register_callback(self._dump_shutdown)
|
||||
# Status reporting
|
||||
def get_status(self, eventtime):
|
||||
if eventtime < self.next_status_time or not self.trapqs:
|
||||
return self.last_status
|
||||
self.next_status_time = eventtime + STATUS_REFRESH_TIME
|
||||
xyzpos = (0., 0., 0.)
|
||||
epos = (0.,)
|
||||
xyzvelocity = evelocity = 0.
|
||||
# Calculate current requested toolhead position
|
||||
mcu = self.printer.lookup_object('mcu')
|
||||
print_time = mcu.estimated_print_time(eventtime)
|
||||
pos, velocity = self.trapqs['toolhead'].get_trapq_position(print_time)
|
||||
if pos is not None:
|
||||
xyzpos = pos[:3]
|
||||
xyzvelocity = velocity
|
||||
# Calculate requested position of currently active extruder
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
ehandler = self.trapqs.get(toolhead.get_extruder().get_name())
|
||||
if ehandler is not None:
|
||||
pos, velocity = ehandler.get_trapq_position(print_time)
|
||||
if pos is not None:
|
||||
epos = (pos[0],)
|
||||
evelocity = velocity
|
||||
# Report status
|
||||
self.last_status = dict(self.last_status)
|
||||
self.last_status['live_position'] = toolhead.Coord(*(xyzpos + epos))
|
||||
self.last_status['live_velocity'] = xyzvelocity
|
||||
self.last_status['live_extruder_velocity'] = evelocity
|
||||
return self.last_status
|
||||
|
||||
def load_config(config):
|
||||
return PrinterMotionReport(config)
|
||||
@@ -0,0 +1,54 @@
|
||||
# Virtual pin that propagates its changes to multiple output pins
|
||||
#
|
||||
# Copyright (C) 2017-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
class PrinterMultiPin:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
ppins = self.printer.lookup_object('pins')
|
||||
try:
|
||||
ppins.register_chip('multi_pin', self)
|
||||
except ppins.error:
|
||||
pass
|
||||
self.pin_type = None
|
||||
self.pin_list = config.getlist('pins')
|
||||
self.mcu_pins = []
|
||||
def setup_pin(self, pin_type, pin_params):
|
||||
ppins = self.printer.lookup_object('pins')
|
||||
pin_name = pin_params['pin']
|
||||
pin = self.printer.lookup_object('multi_pin ' + pin_name, None)
|
||||
if pin is not self:
|
||||
if pin is None:
|
||||
raise ppins.error("""{"code":"key40", "msg":"multi_pin %s not configured", "values": ["%s"]}""" % (pin_name, pin_name))
|
||||
return pin.setup_pin(pin_type, pin_params)
|
||||
if self.pin_type is not None:
|
||||
raise ppins.error("Can't setup multi_pin %s twice" % (pin_name,))
|
||||
self.pin_type = pin_type
|
||||
invert = ""
|
||||
if pin_params['invert']:
|
||||
invert = "!"
|
||||
self.mcu_pins = [ppins.setup_pin(pin_type, invert + pin_desc)
|
||||
for pin_desc in self.pin_list]
|
||||
return self
|
||||
def get_mcu(self):
|
||||
return self.mcu_pins[0].get_mcu()
|
||||
def setup_max_duration(self, max_duration):
|
||||
for mcu_pin in self.mcu_pins:
|
||||
mcu_pin.setup_max_duration(max_duration)
|
||||
def setup_start_value(self, start_value, shutdown_value):
|
||||
for mcu_pin in self.mcu_pins:
|
||||
mcu_pin.setup_start_value(start_value, shutdown_value)
|
||||
def setup_cycle_time(self, cycle_time, hardware_pwm=False):
|
||||
for mcu_pin in self.mcu_pins:
|
||||
mcu_pin.setup_cycle_time(cycle_time, hardware_pwm)
|
||||
def set_digital(self, print_time, value):
|
||||
for mcu_pin in self.mcu_pins:
|
||||
mcu_pin.set_digital(print_time, value)
|
||||
def set_pwm(self, print_time, value, cycle_time=None):
|
||||
for mcu_pin in self.mcu_pins:
|
||||
mcu_pin.set_pwm(print_time, value, cycle_time)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterMultiPin(config)
|
||||
@@ -0,0 +1,112 @@
|
||||
# Support for "neopixel" 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
|
||||
|
||||
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
|
||||
|
||||
BIT_MAX_TIME=.000004
|
||||
RESET_MIN_TIME=.000050
|
||||
|
||||
MAX_MCU_SIZE = 500 # Sanity check on LED chain length
|
||||
|
||||
class PrinterNeoPixel:
|
||||
def __init__(self, config):
|
||||
self.printer = printer = config.get_printer()
|
||||
self.mutex = printer.get_reactor().mutex()
|
||||
# Configure neopixel
|
||||
ppins = printer.lookup_object('pins')
|
||||
pin_params = ppins.lookup_pin(config.get('pin'))
|
||||
self.mcu = pin_params['chip']
|
||||
self.oid = self.mcu.create_oid()
|
||||
self.pin = pin_params['pin']
|
||||
self.mcu.register_config_callback(self.build_config)
|
||||
self.neopixel_update_cmd = self.neopixel_send_cmd = None
|
||||
# Build color map
|
||||
chain_count = config.getint('chain_count', 1, minval=1)
|
||||
color_order = config.getlist("color_order", ["GRB"])
|
||||
if len(color_order) == 1:
|
||||
color_order = [color_order[0]] * chain_count
|
||||
if len(color_order) != chain_count:
|
||||
raise config.error("color_order does not match chain_count")
|
||||
color_indexes = []
|
||||
for lidx, co in enumerate(color_order):
|
||||
if sorted(co) not in (sorted("RGB"), sorted("RGBW")):
|
||||
raise config.error("Invalid color_order '%s'" % (co,))
|
||||
color_indexes.extend([(lidx, "RGBW".index(c)) for c in co])
|
||||
self.color_map = list(enumerate(color_indexes))
|
||||
if len(self.color_map) > MAX_MCU_SIZE:
|
||||
raise config.error("neopixel chain too long")
|
||||
# Initialize color data
|
||||
pled = printer.load_object(config, "led")
|
||||
self.led_helper = pled.setup_helper(config, self.update_leds,
|
||||
chain_count)
|
||||
self.color_data = bytearray(len(self.color_map))
|
||||
self.update_color_data(self.led_helper.get_status()['color_data'])
|
||||
self.old_color_data = bytearray([d ^ 1 for d in self.color_data])
|
||||
# Register callbacks
|
||||
printer.register_event_handler("klippy:connect", self.send_data)
|
||||
def build_config(self):
|
||||
bmt = self.mcu.seconds_to_clock(BIT_MAX_TIME)
|
||||
rmt = self.mcu.seconds_to_clock(RESET_MIN_TIME)
|
||||
self.mcu.add_config_cmd("config_neopixel oid=%d pin=%s data_size=%d"
|
||||
" bit_max_ticks=%d reset_min_ticks=%d"
|
||||
% (self.oid, self.pin, len(self.color_data),
|
||||
bmt, rmt))
|
||||
cmd_queue = self.mcu.alloc_command_queue()
|
||||
self.neopixel_update_cmd = self.mcu.lookup_command(
|
||||
"neopixel_update oid=%c pos=%hu data=%*s", cq=cmd_queue)
|
||||
self.neopixel_send_cmd = self.mcu.lookup_query_command(
|
||||
"neopixel_send oid=%c", "neopixel_result oid=%c success=%c",
|
||||
oid=self.oid, cq=cmd_queue)
|
||||
def update_color_data(self, led_state):
|
||||
color_data = self.color_data
|
||||
for cdidx, (lidx, cidx) in self.color_map:
|
||||
color_data[cdidx] = int(led_state[lidx][cidx] * 255. + .5)
|
||||
def send_data(self, print_time=None):
|
||||
old_data, new_data = self.old_color_data, self.color_data
|
||||
if new_data == old_data:
|
||||
return
|
||||
# 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
|
||||
ucmd = self.neopixel_update_cmd.send
|
||||
for pos, count in diffs:
|
||||
ucmd([self.oid, pos, new_data[pos:pos+count]],
|
||||
reqclock=BACKGROUND_PRIORITY_CLOCK)
|
||||
old_data[:] = new_data
|
||||
# Instruct mcu to update the LEDs
|
||||
minclock = 0
|
||||
if print_time is not None:
|
||||
minclock = self.mcu.print_time_to_clock(print_time)
|
||||
scmd = self.neopixel_send_cmd.send
|
||||
if self.printer.get_start_args().get('debugoutput') is not None:
|
||||
return
|
||||
for i in range(8):
|
||||
params = scmd([self.oid], minclock=minclock,
|
||||
reqclock=BACKGROUND_PRIORITY_CLOCK)
|
||||
if params['success']:
|
||||
break
|
||||
else:
|
||||
logging.info("Neopixel update did not succeed")
|
||||
def update_leds(self, led_state, print_time):
|
||||
def reactor_bgfunc(eventtime):
|
||||
with self.mutex:
|
||||
self.update_color_data(led_state)
|
||||
self.send_data(print_time)
|
||||
self.printer.get_reactor().register_callback(reactor_bgfunc)
|
||||
def get_status(self, eventtime=None):
|
||||
return self.led_helper.get_status(eventtime)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterNeoPixel(config)
|
||||
@@ -0,0 +1,144 @@
|
||||
# Code to configure miscellaneous chips
|
||||
#
|
||||
# Copyright (C) 2017-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
PIN_MIN_TIME = 0.100
|
||||
RESEND_HOST_TIME = 0.300 + PIN_MIN_TIME
|
||||
MAX_SCHEDULE_TIME = 5.0
|
||||
import logging
|
||||
class PrinterOutputPin:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
ppins = self.printer.lookup_object('pins')
|
||||
self.is_pwm = config.getboolean('pwm', False)
|
||||
if self.is_pwm:
|
||||
self.mcu_pin = ppins.setup_pin('pwm', config.get('pin'))
|
||||
cycle_time = config.getfloat('cycle_time', 0.100, above=0.,
|
||||
maxval=MAX_SCHEDULE_TIME)
|
||||
hardware_pwm = config.getboolean('hardware_pwm', False)
|
||||
self.mcu_pin.setup_cycle_time(cycle_time, hardware_pwm)
|
||||
self.scale = config.getfloat('scale', 1., above=0.)
|
||||
self.last_cycle_time = self.default_cycle_time = cycle_time
|
||||
else:
|
||||
self.mcu_pin = ppins.setup_pin('digital_out', config.get('pin'))
|
||||
self.scale = 1.
|
||||
self.last_cycle_time = self.default_cycle_time = 0.
|
||||
self.last_print_time = 0.
|
||||
static_value = config.getfloat('static_value', None,
|
||||
minval=0., maxval=self.scale)
|
||||
self.reactor = self.printer.get_reactor()
|
||||
self.resend_timer = None
|
||||
self.resend_interval = 0.
|
||||
if static_value is not None:
|
||||
self.mcu_pin.setup_max_duration(0.)
|
||||
self.last_value = static_value / self.scale
|
||||
self.mcu_pin.setup_start_value(
|
||||
self.last_value, self.last_value, True)
|
||||
else:
|
||||
max_mcu_duration = config.getfloat('maximum_mcu_duration', 0.,
|
||||
minval=0.500,
|
||||
maxval=MAX_SCHEDULE_TIME)
|
||||
self.mcu_pin.setup_max_duration(max_mcu_duration)
|
||||
if max_mcu_duration:
|
||||
self.resend_interval = max_mcu_duration - RESEND_HOST_TIME
|
||||
|
||||
self.last_value = config.getfloat(
|
||||
'value', 0., minval=0., maxval=self.scale) / self.scale
|
||||
self.shutdown_value = config.getfloat(
|
||||
'shutdown_value', 0., minval=0., maxval=self.scale) / self.scale
|
||||
self.mcu_pin.setup_start_value(self.last_value, self.shutdown_value)
|
||||
pin_name = config.get_name().split()[1]
|
||||
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_mux_command("SET_PIN", "PIN", pin_name,
|
||||
self.cmd_SET_PIN,
|
||||
desc=self.cmd_SET_PIN_help)
|
||||
self.heaters = self.printer.load_object(config,"heaters")
|
||||
|
||||
if pin_name == "power":
|
||||
self.power_timer = self.reactor.register_timer(
|
||||
self.checkpwm, self.reactor.NOW+10)
|
||||
self.ispweron = False
|
||||
def set_poewon(self,value):
|
||||
|
||||
value /= self.scale
|
||||
cycle_time = self.default_cycle_time
|
||||
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.register_lookahead_callback(
|
||||
lambda print_time: self._set_pin(print_time, value, cycle_time))
|
||||
|
||||
|
||||
# toolhead = self.printer.lookup_object('toolhead')
|
||||
# toolhead.register_lookahead_callback(
|
||||
# lambda print_time: self._set_pin(print_time, value, 0))
|
||||
def checkpwm(self, eventtime):
|
||||
systime = self.reactor.monotonic()
|
||||
for heater in self.heaters.heaters.values():
|
||||
|
||||
eventtime = self.reactor.monotonic()
|
||||
if heater.name == "heater_bed" :
|
||||
if heater.check_busy(eventtime) :
|
||||
if self.ispweron == False and heater.target_temp != 0:
|
||||
self.set_poewon(0)
|
||||
self.ispweron = True
|
||||
|
||||
else:
|
||||
if self.ispweron == True:
|
||||
self.ispweron = False
|
||||
self.set_poewon(1)
|
||||
return systime + 10
|
||||
return systime + 3
|
||||
def get_status(self, eventtime):
|
||||
return {'value': self.last_value}
|
||||
def _set_pin(self, print_time, value, cycle_time, is_resend=False):
|
||||
|
||||
|
||||
if value == self.last_value and cycle_time == self.last_cycle_time:
|
||||
if not is_resend:
|
||||
return
|
||||
print_time = max(print_time, self.last_print_time + PIN_MIN_TIME)
|
||||
if self.is_pwm:
|
||||
self.mcu_pin.set_pwm(print_time, value, cycle_time)
|
||||
else:
|
||||
self.mcu_pin.set_digital(print_time, value)
|
||||
self.last_value = value
|
||||
self.last_cycle_time = cycle_time
|
||||
self.last_print_time = print_time
|
||||
if self.resend_interval and self.resend_timer is None:
|
||||
self.resend_timer = self.reactor.register_timer(
|
||||
self._resend_current_val, self.reactor.NOW)
|
||||
|
||||
cmd_SET_PIN_help = "Set the value of an output pin"
|
||||
def cmd_SET_PIN(self, gcmd):
|
||||
|
||||
value = gcmd.get_float('VALUE', minval=0., maxval=self.scale)
|
||||
value /= self.scale
|
||||
cycle_time = gcmd.get_float('CYCLE_TIME', self.default_cycle_time,
|
||||
above=0., maxval=MAX_SCHEDULE_TIME)
|
||||
if not self.is_pwm and value not in [0., 1.]:
|
||||
raise gcmd.error("Invalid pin value")
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.register_lookahead_callback(
|
||||
lambda print_time: self._set_pin(print_time, value, cycle_time))
|
||||
|
||||
def _resend_current_val(self, eventtime):
|
||||
if self.last_value == self.shutdown_value:
|
||||
self.reactor.unregister_timer(self.resend_timer)
|
||||
self.resend_timer = None
|
||||
return self.reactor.NEVER
|
||||
|
||||
systime = self.reactor.monotonic()
|
||||
print_time = self.mcu_pin.get_mcu().estimated_print_time(systime)
|
||||
time_diff = (self.last_print_time + self.resend_interval) - print_time
|
||||
if time_diff > 0.:
|
||||
# Reschedule for resend time
|
||||
return systime + time_diff
|
||||
self._set_pin(print_time + PIN_MIN_TIME,
|
||||
self.last_value, self.last_cycle_time, True)
|
||||
return systime + self.resend_interval
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterOutputPin(config)
|
||||
@@ -0,0 +1,209 @@
|
||||
# Pause/Resume functionality with position capture/restore
|
||||
#
|
||||
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import os, json, logging
|
||||
from .tool import reportInformation
|
||||
|
||||
class PauseResume:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.recover_velocity = config.getfloat('recover_velocity', 50.)
|
||||
self.v_sd = None
|
||||
self.is_paused = False
|
||||
self.sd_paused = False
|
||||
self.pause_command_sent = False
|
||||
self.config = config
|
||||
self.printer.register_event_handler("klippy:connect",
|
||||
self.handle_connect)
|
||||
self.gcode.register_command("PAUSE", self.cmd_PAUSE,
|
||||
desc=self.cmd_PAUSE_help)
|
||||
self.gcode.register_command("RESUME", self.cmd_RESUME,
|
||||
desc=self.cmd_RESUME_help)
|
||||
self.gcode.register_command("CLEAR_PAUSE", self.cmd_CLEAR_PAUSE,
|
||||
desc=self.cmd_CLEAR_PAUSE_help)
|
||||
self.gcode.register_command("CANCEL_PRINT", self.cmd_CANCEL_PRINT,
|
||||
desc=self.cmd_CANCEL_PRINT_help)
|
||||
webhooks = self.printer.lookup_object('webhooks')
|
||||
webhooks.register_endpoint("pause_resume/cancel_continue_print",
|
||||
self._handle_cancel_continue_print_request)
|
||||
webhooks.register_endpoint("pause_resume/check_continue_print_state",
|
||||
self._check_power_loss_state_request)
|
||||
webhooks.register_endpoint("pause_resume/set_print_first_layer",
|
||||
self._set_print_first_layer_request)
|
||||
webhooks.register_endpoint("pause_resume/cancel",
|
||||
self._handle_cancel_request)
|
||||
webhooks.register_endpoint("pause_resume/pause",
|
||||
self._handle_pause_request)
|
||||
webhooks.register_endpoint("pause_resume/resume",
|
||||
self._handle_resume_request)
|
||||
webhooks.register_endpoint("getBootLoaderVersion",
|
||||
self._getBootLoaderVersion)
|
||||
self._setBootLoaderStateCmdOid = None
|
||||
def handle_connect(self):
|
||||
self.v_sd = self.printer.lookup_object('virtual_sdcard', None)
|
||||
|
||||
def _getBootLoaderVersion(self, web_request):
|
||||
mcu = self.printer.lookup_object('mcu')
|
||||
result = mcu.get_constants().get('software_version', '')
|
||||
web_request.send({'software_version': result})
|
||||
return {"software_version": result}
|
||||
|
||||
def _setBootLoaderState(self, web_request):
|
||||
mcu = self.printer.lookup_object('mcu')
|
||||
oid = mcu.create_oid() if not self._setBootLoaderStateCmdOid else self._setBootLoaderStateCmdOid
|
||||
self._setBootLoaderStateCmdOid = oid
|
||||
mcu.add_config_cmd("config_usrboot oid=%d" % (oid,))
|
||||
# sendf("usrboot_ack oid=%c enter_boot_status=%c",args[0],status)
|
||||
result = mcu.lookup_query_command("jump_to_usrboot_query oid=%c", "usrboot_ack oid=%c enter_boot_status=%c", oid=oid).send()
|
||||
return {"result": result}
|
||||
|
||||
def _set_print_first_layer_request(self, web_request):
|
||||
self.v_sd.first_layer_stop = False
|
||||
self.v_sd.print_first_layer = False
|
||||
response = {"state": "success"}
|
||||
web_request.send(response)
|
||||
return response
|
||||
|
||||
def _check_power_loss_state_request(self, web_request):
|
||||
from subprocess import call
|
||||
response = {"file_state": False, "eeprom_state": False}
|
||||
if os.path.exists(self.v_sd.print_file_name_path):
|
||||
try:
|
||||
with open(self.v_sd.print_file_name_path, "r") as f:
|
||||
data = f.read()
|
||||
if len(data) == 0:
|
||||
logging.error("%s f.read()==None read fail!!!" % self.v_sd.print_file_name_path)
|
||||
response["file_state"] = True if json.loads(data) else False
|
||||
except Exception as err:
|
||||
os.remove(self.v_sd.print_file_name_path)
|
||||
bl24c16f = self.printer.lookup_object('bl24c16f') if "bl24c16f" in self.printer.objects else None
|
||||
if bl24c16f:
|
||||
self.gcode.run_script("EEPROM_WRITE_BYTE ADDR=1 VAL=255")
|
||||
logging.exception(err)
|
||||
power_loss_switch = False
|
||||
if os.path.exists(self.v_sd.user_print_refer_path):
|
||||
with open(self.v_sd.user_print_refer_path, "r") as f:
|
||||
data = json.loads(f.read())
|
||||
power_loss_switch = data.get("power_loss", {}).get("switch", False)
|
||||
bl24c16f = self.printer.lookup_object('bl24c16f') if "bl24c16f" in self.printer.objects else None
|
||||
eepromState = bl24c16f.checkEepromFirstEnable() if power_loss_switch and bl24c16f else True
|
||||
if not eepromState:
|
||||
response["eeprom_state"] = True
|
||||
print_stats = self.printer.lookup_object('print_stats', None)
|
||||
if response["file_state"] == True and response["eeprom_state"] == True and print_stats and print_stats.state == "standby":
|
||||
print_stats.power_loss = 1
|
||||
if print_stats and print_stats.state != "standby":
|
||||
response["file_state"] = False
|
||||
response["eeprom_state"] = False
|
||||
logging.info("current printer state:%s" % print_stats.state)
|
||||
if os.path.exists(self.gcode.exclude_object_info) and (response["file_state"]==False or response["eeprom_state"]==False):
|
||||
os.remove(self.gcode.exclude_object_info)
|
||||
web_request.send(response)
|
||||
return response
|
||||
|
||||
def _handle_cancel_continue_print_request(self, web_request):
|
||||
from subprocess import call
|
||||
if os.path.exists(self.v_sd.print_file_name_path):
|
||||
os.remove(self.v_sd.print_file_name_path)
|
||||
if os.path.exists(self.gcode.exclude_object_info):
|
||||
os.remove(self.gcode.exclude_object_info)
|
||||
call("sync", shell=True)
|
||||
bl24c16f = self.printer.lookup_object('bl24c16f') if "bl24c16f" in self.printer.objects else None
|
||||
power_loss_switch = False
|
||||
if os.path.exists(self.v_sd.user_print_refer_path):
|
||||
with open(self.v_sd.user_print_refer_path, "r") as f:
|
||||
data = json.loads(f.read())
|
||||
power_loss_switch = data.get("power_loss", {}).get("switch", False)
|
||||
bl24c16f = self.printer.lookup_object('bl24c16f') if "bl24c16f" in self.printer.objects else None
|
||||
if power_loss_switch and bl24c16f:
|
||||
self.gcode.run_script("EEPROM_WRITE_BYTE ADDR=1 VAL=255")
|
||||
self.gcode.respond_info("cancel_continue_print:success")
|
||||
print_stats = self.printer.lookup_object('print_stats', None)
|
||||
if print_stats:
|
||||
print_stats.power_loss = 0
|
||||
def _handle_cancel_request(self, web_request):
|
||||
self.gcode.run_script("CANCEL_PRINT")
|
||||
def _handle_pause_request(self, web_request):
|
||||
self.gcode.run_script("PAUSE")
|
||||
def _handle_resume_request(self, web_request):
|
||||
self.gcode.run_script("RESUME")
|
||||
def get_status(self, eventtime):
|
||||
return {
|
||||
'is_paused': self.is_paused
|
||||
}
|
||||
def is_sd_active(self):
|
||||
return self.v_sd is not None and self.v_sd.is_active()
|
||||
def send_pause_command(self):
|
||||
# This sends the appropriate pause command from an event. Note
|
||||
# the difference between pause_command_sent and is_paused, the
|
||||
# module isn't officially paused until the PAUSE gcode executes.
|
||||
if not self.pause_command_sent:
|
||||
if self.is_sd_active():
|
||||
# Printing from virtual sd, run pause command
|
||||
self.sd_paused = True
|
||||
self.v_sd.do_pause()
|
||||
else:
|
||||
self.sd_paused = False
|
||||
self.gcode.respond_info("action:paused")
|
||||
self.pause_command_sent = True
|
||||
cmd_PAUSE_help = ("Pauses the current print")
|
||||
def cmd_PAUSE(self, gcmd):
|
||||
import time
|
||||
reactor = self.printer.get_reactor()
|
||||
while self.v_sd.toolhead_moved:
|
||||
time.sleep(0.001)
|
||||
reactor.pause(reactor.monotonic() + .01)
|
||||
if self.is_paused:
|
||||
gcmd.respond_info("""{"code":"key211", "msg": "Print already paused", "values": []}""")
|
||||
return
|
||||
self.send_pause_command()
|
||||
self.gcode.run_script_from_command("SAVE_GCODE_STATE NAME=PAUSE_STATE")
|
||||
self.is_paused = True
|
||||
reportInformation("key601")
|
||||
def send_resume_command(self):
|
||||
if self.sd_paused:
|
||||
# Printing from virtual sd, run pause command
|
||||
self.v_sd.do_resume_status = True
|
||||
self.v_sd.do_resume()
|
||||
self.sd_paused = False
|
||||
else:
|
||||
self.gcode.respond_info("action:resumed")
|
||||
self.pause_command_sent = False
|
||||
cmd_RESUME_help = ("Resumes the print from a pause")
|
||||
def cmd_RESUME(self, gcmd):
|
||||
if not self.is_paused:
|
||||
gcmd.respond_info("""{"code": "key16", "msg": "Print is not paused, resume aborted"}""")
|
||||
return
|
||||
velocity = gcmd.get_float('VELOCITY', self.recover_velocity)
|
||||
self.gcode.run_script_from_command(
|
||||
"RESTORE_GCODE_STATE NAME=PAUSE_STATE MOVE=1 MOVE_SPEED=%.4f"
|
||||
% (velocity))
|
||||
self.send_resume_command()
|
||||
self.is_paused = False
|
||||
result = {}
|
||||
if os.path.exists(self.v_sd.print_file_name_path):
|
||||
with open(self.v_sd.print_file_name_path, "r") as f:
|
||||
result = (json.loads(f.read()))
|
||||
result["variable_z_safe_pause"] = 0
|
||||
with open(self.v_sd.print_file_name_path, "w") as f:
|
||||
f.write(json.dumps(result))
|
||||
f.flush()
|
||||
reportInformation("key602")
|
||||
cmd_CLEAR_PAUSE_help = (
|
||||
"Clears the current paused state without resuming the print")
|
||||
def cmd_CLEAR_PAUSE(self, gcmd):
|
||||
self.is_paused = self.pause_command_sent = False
|
||||
cmd_CANCEL_PRINT_help = ("Cancel the current print")
|
||||
def cmd_CANCEL_PRINT(self, gcmd):
|
||||
if self.is_sd_active() or self.sd_paused:
|
||||
self.v_sd.do_cancel()
|
||||
else:
|
||||
gcmd.respond_info("action:cancel")
|
||||
self.cmd_CLEAR_PAUSE(gcmd)
|
||||
reportInformation("key603")
|
||||
|
||||
def load_config(config):
|
||||
return PauseResume(config)
|
||||
@@ -0,0 +1,144 @@
|
||||
# Calibration of heater PID settings
|
||||
#
|
||||
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import math, logging
|
||||
from . import heaters
|
||||
|
||||
class PIDCalibrate:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command('PID_CALIBRATE', self.cmd_PID_CALIBRATE,
|
||||
desc=self.cmd_PID_CALIBRATE_help)
|
||||
cmd_PID_CALIBRATE_help = "Run PID calibration test"
|
||||
def cmd_PID_CALIBRATE(self, gcmd):
|
||||
heater_name = gcmd.get('HEATER')
|
||||
target = gcmd.get_float('TARGET')
|
||||
write_file = gcmd.get_int('WRITE_FILE', 0)
|
||||
pheaters = self.printer.lookup_object('heaters')
|
||||
try:
|
||||
heater = pheaters.lookup_heater(heater_name)
|
||||
except self.printer.config_error as e:
|
||||
raise gcmd.error(str(e))
|
||||
self.printer.lookup_object('toolhead').get_last_move_time()
|
||||
calibrate = ControlAutoTune(heater, target)
|
||||
old_control = heater.set_control(calibrate)
|
||||
try:
|
||||
pheaters.set_temperature(heater, target, True)
|
||||
except self.printer.command_error as e:
|
||||
heater.set_control(old_control)
|
||||
raise
|
||||
heater.set_control(old_control)
|
||||
if write_file:
|
||||
calibrate.write_file('/tmp/heattest.txt')
|
||||
if calibrate.check_busy(0., 0., 0.):
|
||||
raise gcmd.error('{"code": "key7", "msg": "pid_calibrate interrupted"}')
|
||||
# Log and report results
|
||||
Kp, Ki, Kd = calibrate.calc_final_pid()
|
||||
logging.info("Autotune: final: Kp=%f Ki=%f Kd=%f", Kp, Ki, Kd)
|
||||
gcmd.respond_info(
|
||||
"PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f\n"
|
||||
"The SAVE_CONFIG command will update the printer config file\n"
|
||||
"with these parameters and restart the printer." % (Kp, Ki, Kd))
|
||||
# Store results for SAVE_CONFIG
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
configfile.set(heater_name, 'control', 'pid')
|
||||
configfile.set(heater_name, 'pid_Kp', "%.3f" % (Kp,))
|
||||
configfile.set(heater_name, 'pid_Ki', "%.3f" % (Ki,))
|
||||
configfile.set(heater_name, 'pid_Kd', "%.3f" % (Kd,))
|
||||
|
||||
TUNE_PID_DELTA = 5.0
|
||||
|
||||
class ControlAutoTune:
|
||||
def __init__(self, heater, target):
|
||||
self.heater = heater
|
||||
self.heater_max_power = heater.get_max_power()
|
||||
self.calibrate_temp = target
|
||||
# Heating control
|
||||
self.heating = False
|
||||
self.peak = 0.
|
||||
self.peak_time = 0.
|
||||
# Peak recording
|
||||
self.peaks = []
|
||||
# Sample recording
|
||||
self.last_pwm = 0.
|
||||
self.pwm_samples = []
|
||||
self.temp_samples = []
|
||||
# Heater control
|
||||
def set_pwm(self, read_time, value):
|
||||
if value != self.last_pwm:
|
||||
self.pwm_samples.append(
|
||||
(read_time + self.heater.get_pwm_delay(), value))
|
||||
self.last_pwm = value
|
||||
self.heater.set_pwm(read_time, value)
|
||||
def temperature_update(self, read_time, temp, target_temp):
|
||||
self.temp_samples.append((read_time, temp))
|
||||
# Check if the temperature has crossed the target and
|
||||
# enable/disable the heater if so.
|
||||
if self.heating and temp >= target_temp:
|
||||
self.heating = False
|
||||
self.check_peaks()
|
||||
self.heater.alter_target(self.calibrate_temp - TUNE_PID_DELTA)
|
||||
elif not self.heating and temp <= target_temp:
|
||||
self.heating = True
|
||||
self.check_peaks()
|
||||
self.heater.alter_target(self.calibrate_temp)
|
||||
# Check if this temperature is a peak and record it if so
|
||||
if self.heating:
|
||||
self.set_pwm(read_time, self.heater_max_power)
|
||||
if temp < self.peak:
|
||||
self.peak = temp
|
||||
self.peak_time = read_time
|
||||
else:
|
||||
self.set_pwm(read_time, 0.)
|
||||
if temp > self.peak:
|
||||
self.peak = temp
|
||||
self.peak_time = read_time
|
||||
def check_busy(self, eventtime, smoothed_temp, target_temp):
|
||||
if self.heating or len(self.peaks) < 12:
|
||||
return True
|
||||
return False
|
||||
# Analysis
|
||||
def check_peaks(self):
|
||||
self.peaks.append((self.peak, self.peak_time))
|
||||
if self.heating:
|
||||
self.peak = 9999999.
|
||||
else:
|
||||
self.peak = -9999999.
|
||||
if len(self.peaks) < 4:
|
||||
return
|
||||
self.calc_pid(len(self.peaks)-1)
|
||||
def calc_pid(self, pos):
|
||||
temp_diff = self.peaks[pos][0] - self.peaks[pos-1][0]
|
||||
time_diff = self.peaks[pos][1] - self.peaks[pos-2][1]
|
||||
# Use Astrom-Hagglund method to estimate Ku and Tu
|
||||
amplitude = .5 * abs(temp_diff)
|
||||
Ku = 4. * self.heater_max_power / (math.pi * amplitude)
|
||||
Tu = time_diff
|
||||
# Use Ziegler-Nichols method to generate PID parameters
|
||||
Ti = 0.5 * Tu
|
||||
Td = 0.125 * Tu
|
||||
Kp = 0.6 * Ku * heaters.PID_PARAM_BASE
|
||||
Ki = Kp / Ti
|
||||
Kd = Kp * Td
|
||||
logging.info("Autotune: raw=%f/%f Ku=%f Tu=%f Kp=%f Ki=%f Kd=%f",
|
||||
temp_diff, self.heater_max_power, Ku, Tu, Kp, Ki, Kd)
|
||||
return Kp, Ki, Kd
|
||||
def calc_final_pid(self):
|
||||
cycle_times = [(self.peaks[pos][1] - self.peaks[pos-2][1], pos)
|
||||
for pos in range(4, len(self.peaks))]
|
||||
midpoint_pos = sorted(cycle_times)[len(cycle_times)//2][1]
|
||||
return self.calc_pid(midpoint_pos)
|
||||
# Offline analysis helper
|
||||
def write_file(self, filename):
|
||||
pwm = ["pwm: %.3f %.3f" % (time, value)
|
||||
for time, value in self.pwm_samples]
|
||||
out = ["%.3f %.3f" % (time, temp) for time, temp in self.temp_samples]
|
||||
f = open(filename, "w")
|
||||
f.write('\n'.join(pwm + out))
|
||||
f.close()
|
||||
|
||||
def load_config(config):
|
||||
return PIDCalibrate(config)
|
||||
@@ -0,0 +1,155 @@
|
||||
# Virtual SDCard print stat tracking
|
||||
#
|
||||
# Copyright (C) 2020 Eric Callahan <arksine.code@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import os, json, logging
|
||||
class PrintStats:
|
||||
def __init__(self, config):
|
||||
printer = config.get_printer()
|
||||
self.gcode_move = printer.load_object(config, 'gcode_move')
|
||||
self.reactor = printer.get_reactor()
|
||||
self.reset()
|
||||
# Register commands
|
||||
self.gcode = printer.lookup_object('gcode')
|
||||
self.gcode.register_command(
|
||||
"SET_PRINT_STATS_INFO", self.cmd_SET_PRINT_STATS_INFO,
|
||||
desc=self.cmd_SET_PRINT_STATS_INFO_help)
|
||||
# G28 down 12mm flag
|
||||
self.power_loss = 0
|
||||
self.print_duration = 0
|
||||
self.z_pos_filepath = "/usr/data/creality/userdata/config/z_pos.json"
|
||||
self.z_pos = self.get_z_pos()
|
||||
def get_z_pos(self):
|
||||
z_pos = 0
|
||||
if os.path.exists(self.z_pos_filepath):
|
||||
try:
|
||||
with open(self.z_pos_filepath, "r") as f:
|
||||
z_pos = float(json.loads(f.read()).get("z_pos", 0))
|
||||
except Exception as err:
|
||||
logging.error(err)
|
||||
return z_pos
|
||||
def _update_filament_usage(self, eventtime):
|
||||
gc_status = self.gcode_move.get_status(eventtime)
|
||||
cur_epos = gc_status['position'].e
|
||||
self.filament_used += (cur_epos - self.last_epos) \
|
||||
/ gc_status['extrude_factor']
|
||||
self.last_epos = cur_epos
|
||||
def set_current_file(self, filename):
|
||||
self.reset()
|
||||
self.filename = filename
|
||||
def note_start(self, info_path=""):
|
||||
curtime = self.reactor.monotonic()
|
||||
# if self.print_start_time is None:
|
||||
# self.print_start_time = curtime
|
||||
# elif self.last_pause_time is not None:
|
||||
# # Update pause time duration
|
||||
# pause_duration = curtime - self.last_pause_time
|
||||
# self.prev_pause_duration += pause_duration
|
||||
# self.last_pause_time = None
|
||||
# Reset last e-position
|
||||
gc_status = self.gcode_move.get_status(curtime)
|
||||
ret = {}
|
||||
if info_path and os.path.exists(info_path):
|
||||
try:
|
||||
with open(info_path, "r") as f:
|
||||
ret = json.loads(f.read())
|
||||
self.filament_used = ret.get("filament_used", 0)
|
||||
except Exception as err:
|
||||
pass
|
||||
if self.print_start_time is None:
|
||||
if info_path and ret and ret.get("last_print_duration"):
|
||||
self.print_start_time = curtime - int(ret.get("last_print_duration", 0))
|
||||
else:
|
||||
self.print_start_time = curtime
|
||||
elif self.last_pause_time is not None:
|
||||
# Update pause time duration
|
||||
pause_duration = curtime - self.last_pause_time
|
||||
self.prev_pause_duration += pause_duration
|
||||
self.last_pause_time = None
|
||||
self.last_epos = gc_status['position'].e
|
||||
self.state = "printing"
|
||||
self.error_message = ""
|
||||
def note_pause(self):
|
||||
if self.last_pause_time is None:
|
||||
curtime = self.reactor.monotonic()
|
||||
self.last_pause_time = curtime
|
||||
# update filament usage
|
||||
self._update_filament_usage(curtime)
|
||||
if self.state != "error":
|
||||
self.state = "paused"
|
||||
def note_complete(self):
|
||||
self._note_finish("complete")
|
||||
def note_error(self, message):
|
||||
self._note_finish("error", message)
|
||||
def note_cancel(self):
|
||||
self._note_finish("cancelled")
|
||||
def _note_finish(self, state, error_message = ""):
|
||||
if self.print_start_time is None:
|
||||
return
|
||||
self.state = state
|
||||
self.error_message = error_message
|
||||
eventtime = self.reactor.monotonic()
|
||||
self.total_duration = eventtime - self.print_start_time
|
||||
if self.filament_used < 0.0000001:
|
||||
# No positive extusion detected during print
|
||||
self.init_duration = self.total_duration - \
|
||||
self.prev_pause_duration
|
||||
self.print_start_time = None
|
||||
cmd_SET_PRINT_STATS_INFO_help = "Pass slicer info like layer act and " \
|
||||
"total to klipper"
|
||||
def cmd_SET_PRINT_STATS_INFO(self, gcmd):
|
||||
total_layer = gcmd.get_int("TOTAL_LAYER", self.info_total_layer, \
|
||||
minval=0)
|
||||
current_layer = gcmd.get_int("CURRENT_LAYER", self.info_current_layer, \
|
||||
minval=0)
|
||||
if total_layer == 0:
|
||||
self.info_total_layer = None
|
||||
self.info_current_layer = None
|
||||
elif total_layer != self.info_total_layer:
|
||||
self.info_total_layer = total_layer
|
||||
self.info_current_layer = 0
|
||||
|
||||
if self.info_total_layer is not None and \
|
||||
current_layer is not None and \
|
||||
current_layer != self.info_current_layer:
|
||||
self.info_current_layer = min(current_layer, self.info_total_layer)
|
||||
def reset(self):
|
||||
self.filename = self.error_message = ""
|
||||
self.state = "standby"
|
||||
self.prev_pause_duration = self.last_epos = 0.
|
||||
self.filament_used = self.total_duration = 0.
|
||||
self.print_start_time = self.last_pause_time = None
|
||||
self.init_duration = 0.
|
||||
self.info_total_layer = None
|
||||
self.info_current_layer = None
|
||||
def get_status(self, eventtime):
|
||||
time_paused = self.prev_pause_duration
|
||||
if self.print_start_time is not None:
|
||||
if self.last_pause_time is not None:
|
||||
# Calculate the total time spent paused during the print
|
||||
time_paused += eventtime - self.last_pause_time
|
||||
else:
|
||||
# Accumulate filament if not paused
|
||||
self._update_filament_usage(eventtime)
|
||||
self.total_duration = eventtime - self.print_start_time
|
||||
if self.filament_used < 0.0000001:
|
||||
# Track duration prior to extrusion
|
||||
self.init_duration = self.total_duration - time_paused
|
||||
print_duration = self.total_duration - self.init_duration - time_paused
|
||||
self.print_duration = print_duration
|
||||
return {
|
||||
'filename': self.filename,
|
||||
'total_duration': self.total_duration,
|
||||
'print_duration': print_duration,
|
||||
'filament_used': self.filament_used,
|
||||
'state': self.state,
|
||||
'message': self.error_message,
|
||||
'info': {'total_layer': self.info_total_layer,
|
||||
'current_layer': self.info_current_layer},
|
||||
'power_loss': self.power_loss,
|
||||
'z_pos': self.z_pos,
|
||||
}
|
||||
|
||||
def load_config(config):
|
||||
return PrintStats(config)
|
||||
Executable
+471
@@ -0,0 +1,471 @@
|
||||
# Z-Probe support
|
||||
#
|
||||
# Copyright (C) 2017-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
import pins
|
||||
from . import manual_probe
|
||||
|
||||
HINT_TIMEOUT = """
|
||||
If the probe did not move far enough to trigger, then
|
||||
consider reducing the Z axis minimum position so the probe
|
||||
can travel further (the Z minimum position can be negative).
|
||||
"""
|
||||
|
||||
class PrinterProbe:
|
||||
def __init__(self, config, mcu_probe):
|
||||
self.printer = config.get_printer()
|
||||
self.name = config.get_name()
|
||||
self.mcu_probe = mcu_probe
|
||||
self.speed = config.getfloat('speed', 5.0, above=0.)
|
||||
self.lift_speed = config.getfloat('lift_speed', self.speed, above=0.)
|
||||
self.x_offset = config.getfloat('x_offset', 0.)
|
||||
self.y_offset = config.getfloat('y_offset', 0.)
|
||||
self.z_offset = config.getfloat('z_offset')
|
||||
self.z_offset_calibrate = 0
|
||||
self.z_offset_change_flag = False
|
||||
self.probe_calibrate_z = 0.
|
||||
self.multi_probe_pending = False
|
||||
self.last_state = False
|
||||
self.last_z_result = 0.
|
||||
self.gcode_move = self.printer.load_object(config, "gcode_move")
|
||||
# Infer Z position to move to during a probe
|
||||
if config.has_section('stepper_z'):
|
||||
zconfig = config.getsection('stepper_z')
|
||||
self.z_position = zconfig.getfloat('position_min', 0.,
|
||||
note_valid=False)
|
||||
else:
|
||||
pconfig = config.getsection('printer')
|
||||
self.z_position = pconfig.getfloat('minimum_z_position', 0.,
|
||||
note_valid=False)
|
||||
# Multi-sample support (for improved accuracy)
|
||||
self.sample_count = config.getint('samples', 1, minval=1)
|
||||
self.sample_retract_dist = config.getfloat('sample_retract_dist', 2.,
|
||||
above=0.)
|
||||
atypes = {'median': 'median', 'average': 'average'}
|
||||
self.samples_result = config.getchoice('samples_result', atypes,
|
||||
'average')
|
||||
self.samples_tolerance = config.getfloat('samples_tolerance', 0.100,
|
||||
minval=0.)
|
||||
self.samples_retries = config.getint('samples_tolerance_retries', 0,
|
||||
minval=0)
|
||||
# Register z_virtual_endstop pin
|
||||
self.printer.lookup_object('pins').register_chip('probe', self)
|
||||
# Register homing event handlers
|
||||
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.printer.register_event_handler("homing:home_rails_begin",
|
||||
self._handle_home_rails_begin)
|
||||
self.printer.register_event_handler("homing:home_rails_end",
|
||||
self._handle_home_rails_end)
|
||||
self.printer.register_event_handler("gcode:command_error",
|
||||
self._handle_command_error)
|
||||
# Register PROBE/QUERY_PROBE commands
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.gcode.register_command('PROBE', self.cmd_PROBE,
|
||||
desc=self.cmd_PROBE_help)
|
||||
self.gcode.register_command('QUERY_PROBE', self.cmd_QUERY_PROBE,
|
||||
desc=self.cmd_QUERY_PROBE_help)
|
||||
self.gcode.register_command('PROBE_CALIBRATE', self.cmd_PROBE_CALIBRATE,
|
||||
desc=self.cmd_PROBE_CALIBRATE_help)
|
||||
self.gcode.register_command('PROBE_ACCURACY', self.cmd_PROBE_ACCURACY,
|
||||
desc=self.cmd_PROBE_ACCURACY_help)
|
||||
self.gcode.register_command('Z_OFFSET_APPLY_PROBE',
|
||||
self.cmd_Z_OFFSET_APPLY_PROBE,
|
||||
desc=self.cmd_Z_OFFSET_APPLY_PROBE_help)
|
||||
def _handle_homing_move_begin(self, hmove):
|
||||
if self.mcu_probe in hmove.get_mcu_endstops():
|
||||
self.mcu_probe.probe_prepare(hmove)
|
||||
def _handle_homing_move_end(self, hmove):
|
||||
if self.mcu_probe in hmove.get_mcu_endstops():
|
||||
self.mcu_probe.probe_finish(hmove)
|
||||
def _handle_home_rails_begin(self, homing_state, rails):
|
||||
endstops = [es for rail in rails for es, name in rail.get_endstops()]
|
||||
if self.mcu_probe in endstops:
|
||||
self.multi_probe_begin()
|
||||
def _handle_home_rails_end(self, homing_state, rails):
|
||||
endstops = [es for rail in rails for es, name in rail.get_endstops()]
|
||||
if self.mcu_probe in endstops:
|
||||
self.multi_probe_end()
|
||||
def _handle_command_error(self):
|
||||
try:
|
||||
self.multi_probe_end()
|
||||
except:
|
||||
logging.exception("Multi-probe end")
|
||||
def multi_probe_begin(self):
|
||||
self.mcu_probe.multi_probe_begin()
|
||||
self.multi_probe_pending = True
|
||||
def multi_probe_end(self):
|
||||
if self.multi_probe_pending:
|
||||
self.multi_probe_pending = False
|
||||
self.mcu_probe.multi_probe_end()
|
||||
def setup_pin(self, pin_type, pin_params):
|
||||
if pin_type != 'endstop' or pin_params['pin'] != 'z_virtual_endstop':
|
||||
raise pins.error("Probe virtual endstop only useful as endstop pin")
|
||||
if pin_params['invert'] or pin_params['pullup']:
|
||||
raise pins.error("Can not pullup/invert probe virtual endstop")
|
||||
return self.mcu_probe
|
||||
def get_lift_speed(self, gcmd=None):
|
||||
if gcmd is not None:
|
||||
return gcmd.get_float("LIFT_SPEED", self.lift_speed, above=0.)
|
||||
return self.lift_speed
|
||||
def get_offsets(self):
|
||||
return self.x_offset, self.y_offset, self.z_offset
|
||||
def _probe(self, speed, gcmd=None):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
curtime = self.printer.get_reactor().monotonic()
|
||||
if 'z' not in toolhead.get_status(curtime)['homed_axes']:
|
||||
raise self.printer.command_error("""{"code":"key96", "msg": "Must home before probe", "values": []}""")
|
||||
phoming = self.printer.lookup_object('homing')
|
||||
pos = toolhead.get_position()
|
||||
pos[2] = self.z_position
|
||||
try:
|
||||
epos = phoming.probing_move(self.mcu_probe, pos, speed)
|
||||
except self.printer.command_error as e:
|
||||
reason = str(e)
|
||||
if "Timeout during endstop homing" in reason:
|
||||
reason += HINT_TIMEOUT
|
||||
raise self.printer.command_error(reason)
|
||||
msg = "probe at %.3f,%.3f is z=%.6f" % (epos[0], epos[1], epos[2] - self.z_offset)
|
||||
if gcmd and gcmd.get_commandline().startswith("Z_OFFSET_AUTO"):
|
||||
msg = "Z_OFFSET_AUTO probe at %.3f,%.3f is z=%.6f" % (epos[0], epos[1], epos[2] - self.z_offset)
|
||||
self.gcode.respond_info(msg)
|
||||
return epos[:3]
|
||||
def _move(self, coord, speed):
|
||||
self.printer.lookup_object('toolhead').manual_move(coord, speed)
|
||||
def _calc_mean(self, positions):
|
||||
count = float(len(positions))
|
||||
return [sum([pos[i] for pos in positions]) / count
|
||||
for i in range(3)]
|
||||
def _calc_median(self, positions):
|
||||
z_sorted = sorted(positions, key=(lambda p: p[2]))
|
||||
middle = len(positions) // 2
|
||||
if (len(positions) & 1) == 1:
|
||||
# odd number of samples
|
||||
return z_sorted[middle]
|
||||
# even number of samples
|
||||
return self._calc_mean(z_sorted[middle-1:middle+1])
|
||||
def run_probe(self, gcmd):
|
||||
speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.)
|
||||
lift_speed = self.get_lift_speed(gcmd)
|
||||
sample_count = gcmd.get_int("SAMPLES", self.sample_count, minval=1)
|
||||
sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST",
|
||||
self.sample_retract_dist, above=0.)
|
||||
samples_tolerance = gcmd.get_float("SAMPLES_TOLERANCE",
|
||||
self.samples_tolerance, minval=0.)
|
||||
samples_retries = gcmd.get_int("SAMPLES_TOLERANCE_RETRIES",
|
||||
self.samples_retries, minval=0)
|
||||
samples_result = gcmd.get("SAMPLES_RESULT", self.samples_result)
|
||||
must_notify_multi_probe = not self.multi_probe_pending
|
||||
if must_notify_multi_probe:
|
||||
self.multi_probe_begin()
|
||||
probexy = self.printer.lookup_object('toolhead').get_position()[:2]
|
||||
retries = 0
|
||||
positions = []
|
||||
while len(positions) < sample_count:
|
||||
# Probe position
|
||||
try:
|
||||
pos = self._probe(speed, gcmd)
|
||||
except Exception as err:
|
||||
reason = str(err)
|
||||
logging.error(reason)
|
||||
epos_state = False
|
||||
if "Communication timeout during homing" in reason:
|
||||
count = 5
|
||||
while count:
|
||||
count -= 1
|
||||
self._move(probexy + [2 + sample_retract_dist], lift_speed)
|
||||
self.printer.get_reactor().pause(self.printer.get_reactor().monotonic() + 5.0)
|
||||
try:
|
||||
pos = self._probe(speed)
|
||||
epos_state = True
|
||||
break
|
||||
except Exception as err:
|
||||
logging.error(err)
|
||||
if not epos_state:
|
||||
raise self.printer.command_error(reason)
|
||||
positions.append(pos)
|
||||
# Check samples tolerance
|
||||
z_positions = [p[2] for p in positions]
|
||||
if max(z_positions) - min(z_positions) > samples_tolerance:
|
||||
if retries >= samples_retries:
|
||||
raise gcmd.error("Probe samples exceed samples_tolerance")
|
||||
gcmd.respond_info("Probe samples exceed tolerance. Retrying...")
|
||||
retries += 1
|
||||
positions = []
|
||||
# Retract
|
||||
if len(positions) < sample_count:
|
||||
self._move(probexy + [pos[2] + sample_retract_dist], lift_speed)
|
||||
if must_notify_multi_probe:
|
||||
self.multi_probe_end()
|
||||
# Calculate and return result
|
||||
if samples_result == 'median':
|
||||
return self._calc_median(positions)
|
||||
return self._calc_mean(positions)
|
||||
cmd_PROBE_help = "Probe Z-height at current XY position"
|
||||
def cmd_PROBE(self, gcmd):
|
||||
pos = self.run_probe(gcmd)
|
||||
gcmd.respond_info("Result is z=%.6f" % (pos[2],))
|
||||
self.last_z_result = pos[2]
|
||||
cmd_QUERY_PROBE_help = "Return the status of the z-probe"
|
||||
def cmd_QUERY_PROBE(self, gcmd):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
print_time = toolhead.get_last_move_time()
|
||||
res = self.mcu_probe.query_endstop(print_time)
|
||||
self.last_state = res
|
||||
gcmd.respond_info("probe: %s" % (["open", "TRIGGERED"][not not res],))
|
||||
def get_status(self, eventtime):
|
||||
return {'last_query': self.last_state,
|
||||
'last_z_result': self.last_z_result,
|
||||
'z_offset': self.z_offset_calibrate if self.z_offset_change_flag else self.z_offset}
|
||||
cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position"
|
||||
def cmd_PROBE_ACCURACY(self, gcmd):
|
||||
speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.)
|
||||
lift_speed = self.get_lift_speed(gcmd)
|
||||
sample_count = gcmd.get_int("SAMPLES", 10, minval=1)
|
||||
sample_retract_dist = gcmd.get_float("SAMPLE_RETRACT_DIST",
|
||||
self.sample_retract_dist, above=0.)
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
pos = toolhead.get_position()
|
||||
gcmd.respond_info("PROBE_ACCURACY at X:%.3f Y:%.3f Z:%.3f"
|
||||
" (samples=%d retract=%.3f"
|
||||
" speed=%.1f lift_speed=%.1f)\n"
|
||||
% (pos[0], pos[1], pos[2],
|
||||
sample_count, sample_retract_dist,
|
||||
speed, lift_speed))
|
||||
# Probe bed sample_count times
|
||||
self.multi_probe_begin()
|
||||
positions = []
|
||||
while len(positions) < sample_count:
|
||||
# Probe position
|
||||
pos = self._probe(speed)
|
||||
positions.append(pos)
|
||||
# Retract
|
||||
liftpos = [None, None, pos[2] + sample_retract_dist]
|
||||
self._move(liftpos, lift_speed)
|
||||
self.multi_probe_end()
|
||||
# Calculate maximum, minimum and average values
|
||||
max_value = max([p[2] for p in positions])
|
||||
min_value = min([p[2] for p in positions])
|
||||
range_value = max_value - min_value
|
||||
avg_value = self._calc_mean(positions)[2]
|
||||
median = self._calc_median(positions)[2]
|
||||
# calculate the standard deviation
|
||||
deviation_sum = 0
|
||||
for i in range(len(positions)):
|
||||
deviation_sum += pow(positions[i][2] - avg_value, 2.)
|
||||
sigma = (deviation_sum / len(positions)) ** 0.5
|
||||
# Show information
|
||||
gcmd.respond_info(
|
||||
"probe accuracy results: maximum %.6f, minimum %.6f, range %.6f, "
|
||||
"average %.6f, median %.6f, standard deviation %.6f" % (
|
||||
max_value, min_value, range_value, avg_value, median, sigma))
|
||||
def probe_calibrate_finalize(self, kin_pos):
|
||||
if kin_pos is None:
|
||||
return
|
||||
z_offset = self.probe_calibrate_z - kin_pos[2]
|
||||
self.gcode.respond_info(
|
||||
"%s: z_offset: %.3f\n"
|
||||
"The SAVE_CONFIG command will update the printer config file\n"
|
||||
"with the above and restart the printer." % (self.name, z_offset))
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
configfile.set(self.name, 'z_offset', "%.3f" % (z_offset,))
|
||||
cmd_PROBE_CALIBRATE_help = "Calibrate the probe's z_offset"
|
||||
def cmd_PROBE_CALIBRATE(self, gcmd):
|
||||
manual_probe.verify_no_manual_probe(self.printer)
|
||||
# Perform initial probe
|
||||
lift_speed = self.get_lift_speed(gcmd)
|
||||
curpos = self.run_probe(gcmd)
|
||||
# Move away from the bed
|
||||
self.probe_calibrate_z = curpos[2]
|
||||
curpos[2] += 5.
|
||||
self._move(curpos, lift_speed)
|
||||
# Move the nozzle over the probe point
|
||||
curpos[0] += self.x_offset
|
||||
curpos[1] += self.y_offset
|
||||
self._move(curpos, self.speed)
|
||||
# Start manual probe
|
||||
manual_probe.ManualProbeHelper(self.printer, gcmd,
|
||||
self.probe_calibrate_finalize)
|
||||
def cmd_Z_OFFSET_APPLY_PROBE(self,gcmd):
|
||||
offset = self.gcode_move.get_status()['homing_origin'].z
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
if offset == 0:
|
||||
self.gcode.respond_info("Nothing to do: Z Offset is 0")
|
||||
else:
|
||||
new_calibrate = self.z_offset - offset
|
||||
if new_calibrate < 0:
|
||||
new_calibrate = 0
|
||||
self.gcode.respond_info(
|
||||
"%s: z_offset: %.3f\n"
|
||||
"The SAVE_CONFIG command will update the printer config file\n"
|
||||
"with the above and restart the printer."
|
||||
% (self.name, new_calibrate))
|
||||
configfile.set(self.name, 'z_offset', "%.3f" % (new_calibrate,))
|
||||
self.z_offset_calibrate = new_calibrate
|
||||
self.z_offset_change_flag = True
|
||||
cmd_Z_OFFSET_APPLY_PROBE_help = "Adjust the probe's z_offset"
|
||||
|
||||
# Endstop wrapper that enables probe specific features
|
||||
class ProbeEndstopWrapper:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.position_endstop = config.getfloat('z_offset')
|
||||
self.stow_on_each_sample = config.getboolean(
|
||||
'deactivate_on_each_sample', True)
|
||||
gcode_macro = self.printer.load_object(config, 'gcode_macro')
|
||||
self.activate_gcode = gcode_macro.load_template(
|
||||
config, 'activate_gcode', '')
|
||||
self.deactivate_gcode = gcode_macro.load_template(
|
||||
config, 'deactivate_gcode', '')
|
||||
# Create an "endstop" object to handle the probe pin
|
||||
ppins = self.printer.lookup_object('pins')
|
||||
pin = config.get('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)
|
||||
self.printer.register_event_handler('klippy:mcu_identify',
|
||||
self._handle_mcu_identify)
|
||||
# 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_start = self.mcu_endstop.home_start
|
||||
self.home_wait = self.mcu_endstop.home_wait
|
||||
self.query_endstop = self.mcu_endstop.query_endstop
|
||||
# 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 raise_probe(self):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
start_pos = toolhead.get_position()
|
||||
self.deactivate_gcode.run_gcode_from_command()
|
||||
if toolhead.get_position()[:3] != start_pos[:3]:
|
||||
raise self.printer.command_error(
|
||||
"Toolhead moved during probe activate_gcode script")
|
||||
def lower_probe(self):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
start_pos = toolhead.get_position()
|
||||
self.activate_gcode.run_gcode_from_command()
|
||||
if toolhead.get_position()[:3] != start_pos[:3]:
|
||||
raise self.printer.command_error(
|
||||
"Toolhead moved during probe deactivate_gcode script")
|
||||
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.raise_probe()
|
||||
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'
|
||||
def probe_finish(self, hmove):
|
||||
if self.multi == 'OFF':
|
||||
self.raise_probe()
|
||||
def get_position_endstop(self):
|
||||
return self.position_endstop
|
||||
|
||||
# Helper code that can probe a series of points and report the
|
||||
# position at each point.
|
||||
class ProbePointsHelper:
|
||||
def __init__(self, config, finalize_callback, default_points=None):
|
||||
self.printer = config.get_printer()
|
||||
self.finalize_callback = finalize_callback
|
||||
self.probe_points = default_points
|
||||
self.name = config.get_name()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
# Read config settings
|
||||
if default_points is None or config.get('points', None) is not None:
|
||||
self.probe_points = config.getlists('points', seps=(',', '\n'),
|
||||
parser=float, count=2)
|
||||
self.horizontal_move_z = config.getfloat('horizontal_move_z', 5.)
|
||||
self.speed = config.getfloat('speed', 50., above=0.)
|
||||
self.use_offsets = False
|
||||
# Internal probing state
|
||||
self.lift_speed = self.speed
|
||||
self.probe_offsets = (0., 0., 0.)
|
||||
self.results = []
|
||||
def minimum_points(self,n):
|
||||
if len(self.probe_points) < n:
|
||||
raise self.printer.config_error(
|
||||
"""{"code":"key98", "msg": "Need at least %d probe points for %s", "values": [%d, "%s"]}""" % (n, self.name, n, self.name))
|
||||
def update_probe_points(self, points, min_points):
|
||||
self.probe_points = points
|
||||
self.minimum_points(min_points)
|
||||
def use_xy_offsets(self, use_offsets):
|
||||
self.use_offsets = use_offsets
|
||||
def get_lift_speed(self):
|
||||
return self.lift_speed
|
||||
def _move_next(self):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
# Lift toolhead
|
||||
speed = self.lift_speed
|
||||
if not self.results:
|
||||
# Use full speed to first probe position
|
||||
speed = self.speed
|
||||
toolhead.manual_move([None, None, self.horizontal_move_z], speed)
|
||||
# Check if done probing
|
||||
if len(self.results) >= len(self.probe_points):
|
||||
toolhead.get_last_move_time()
|
||||
res = self.finalize_callback(self.probe_offsets, self.results)
|
||||
if res != "retry":
|
||||
return True
|
||||
self.results = []
|
||||
# Move to next XY probe point
|
||||
nextpos = list(self.probe_points[len(self.results)])
|
||||
if self.use_offsets:
|
||||
nextpos[0] -= self.probe_offsets[0]
|
||||
nextpos[1] -= self.probe_offsets[1]
|
||||
toolhead.manual_move(nextpos, self.speed)
|
||||
return False
|
||||
def start_probe(self, gcmd):
|
||||
manual_probe.verify_no_manual_probe(self.printer)
|
||||
# Lookup objects
|
||||
probe = self.printer.lookup_object('probe', None)
|
||||
method = gcmd.get('METHOD', 'automatic').lower()
|
||||
self.results = []
|
||||
if probe is None or method != 'automatic':
|
||||
# Manual probe
|
||||
self.lift_speed = self.speed
|
||||
self.probe_offsets = (0., 0., 0.)
|
||||
self._manual_probe_start()
|
||||
return
|
||||
# Perform automatic probing
|
||||
self.lift_speed = probe.get_lift_speed(gcmd)
|
||||
self.probe_offsets = probe.get_offsets()
|
||||
if self.horizontal_move_z < self.probe_offsets[2]:
|
||||
raise gcmd.error("""{"code": "key15", "msg": "horizontal_move_z can't be less than probe's z_offset"}""")
|
||||
probe.multi_probe_begin()
|
||||
while 1:
|
||||
done = self._move_next()
|
||||
if done:
|
||||
break
|
||||
pos = probe.run_probe(gcmd)
|
||||
self.results.append(pos)
|
||||
probe.multi_probe_end()
|
||||
def _manual_probe_start(self):
|
||||
done = self._move_next()
|
||||
if not done:
|
||||
gcmd = self.gcode.create_gcode_command("", "", {})
|
||||
manual_probe.ManualProbeHelper(self.printer, gcmd,
|
||||
self._manual_probe_finalize)
|
||||
def _manual_probe_finalize(self, kin_pos):
|
||||
if kin_pos is None:
|
||||
return
|
||||
self.results.append(kin_pos)
|
||||
self._manual_probe_start()
|
||||
|
||||
def load_config(config):
|
||||
return PrinterProbe(config, ProbeEndstopWrapper(config))
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
# prtouch support
|
||||
#
|
||||
# Copyright (C) 2018-2021 Creality <wangyulong878@sina.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
from . import probe
|
||||
from . import prtouch_v2_wrapper
|
||||
|
||||
def load_config(config):
|
||||
vrt = prtouch_v2_wrapper.PRTouchEndstopWrapper(config)
|
||||
# config.get_printer().add_object('probe', probe.PrinterProbe(config, vrt))
|
||||
return vrt
|
||||
|
||||
|
||||
# /home/cc/moonraker-env/bin/python3.10 /home/cc/moonraker/moonraker/moonraker.py -d /home/cc/printer_data
|
||||
# sudo service klipper stop
|
||||
# /home/cc/klippy-env/bin/python3.10 /home/cc/klipper/klippy/klippy.py /home/cc/printer_data/config/printer.cfg -a /home/cc/printer_data/comms/klippy.sock -l /home/cc/printer_data/logs/klippy.log
|
||||
|
||||
# ./micropython /home/cc/klipper/klippy/klippy.py /home/cc/printer_data/config/printer.cfg -a /home/cc/printer_data/comms/klippy.sock -l /home/cc/printer_data/logs/klippy.log
|
||||
|
||||
# /home/cc/klippy-env/bin/python3.10 /home/cc/micropython_test/klipper/klippy/klippy.py /home/cc/printer_data/config/printer.cfg -a /home/cc/printer_data/comms/klippy.sock -l /home/cc/printer_data/logs/klippy.log
|
||||
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
# Support for GPIO input edge counters
|
||||
#
|
||||
# Copyright (C) 2021 Adrian Keet <arkeet@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
class MCU_counter:
|
||||
def __init__(self, printer, pin, sample_time, poll_time):
|
||||
ppins = printer.lookup_object('pins')
|
||||
pin_params = ppins.lookup_pin(pin, can_pullup=True)
|
||||
self._mcu = pin_params['chip']
|
||||
self._oid = self._mcu.create_oid()
|
||||
self._pin = pin_params['pin']
|
||||
self._pullup = pin_params['pullup']
|
||||
self._poll_time = poll_time
|
||||
self._poll_ticks = 0
|
||||
self._sample_time = sample_time
|
||||
self._callback = None
|
||||
self._last_count = 0
|
||||
self._mcu.register_config_callback(self.build_config)
|
||||
|
||||
def build_config(self):
|
||||
self._mcu.add_config_cmd("config_counter oid=%d pin=%s pull_up=%d"
|
||||
% (self._oid, self._pin, self._pullup))
|
||||
clock = self._mcu.get_query_slot(self._oid)
|
||||
self._poll_ticks = self._mcu.seconds_to_clock(self._poll_time)
|
||||
sample_ticks = self._mcu.seconds_to_clock(self._sample_time)
|
||||
self._mcu.add_config_cmd(
|
||||
"query_counter oid=%d clock=%d poll_ticks=%d sample_ticks=%d"
|
||||
% (self._oid, clock, self._poll_ticks, sample_ticks), is_init=True)
|
||||
self._mcu.register_response(self._handle_counter_state,
|
||||
"counter_state", self._oid)
|
||||
|
||||
# Callback is called periodically every sample_time
|
||||
def setup_callback(self, cb):
|
||||
self._callback = cb
|
||||
|
||||
def _handle_counter_state(self, params):
|
||||
next_clock = self._mcu.clock32_to_clock64(params['next_clock'])
|
||||
time = self._mcu.clock_to_print_time(next_clock - self._poll_ticks)
|
||||
|
||||
count_clock = self._mcu.clock32_to_clock64(params['count_clock'])
|
||||
count_time = self._mcu.clock_to_print_time(count_clock)
|
||||
|
||||
# handle 32-bit counter overflow
|
||||
last_count = self._last_count
|
||||
delta_count = (params['count'] - last_count) & 0xffffffff
|
||||
count = last_count + delta_count
|
||||
self._last_count = count
|
||||
|
||||
if self._callback is not None:
|
||||
self._callback(time, count, count_time)
|
||||
|
||||
class FrequencyCounter:
|
||||
def __init__(self, printer, pin, sample_time, poll_time):
|
||||
self._callback = None
|
||||
self._last_time = self._last_count = None
|
||||
self._freq = 0.
|
||||
self._counter = MCU_counter(printer, pin, sample_time, poll_time)
|
||||
self._counter.setup_callback(self._counter_callback)
|
||||
|
||||
def _counter_callback(self, time, count, count_time):
|
||||
if self._last_time is None: # First sample
|
||||
self._last_time = time
|
||||
else:
|
||||
delta_time = count_time - self._last_time
|
||||
if delta_time > 0:
|
||||
self._last_time = count_time
|
||||
delta_count = count - self._last_count
|
||||
self._freq = delta_count / delta_time
|
||||
else: # No counts since last sample
|
||||
self._last_time = time
|
||||
self._freq = 0.
|
||||
if self._callback is not None:
|
||||
self._callback(time, self._freq)
|
||||
self._last_count = count
|
||||
|
||||
def get_frequency(self):
|
||||
return self._freq
|
||||
@@ -0,0 +1,126 @@
|
||||
# Mechanicaly conforms a moving gantry to the bed with 4 Z steppers
|
||||
#
|
||||
# Copyright (C) 2018 Maks Zolin <mzolin@vorondesign.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
from . import probe, z_tilt
|
||||
|
||||
# Leveling code for XY rails that are controlled by Z steppers as in:
|
||||
#
|
||||
# Z stepper1 ----> O O <---- Z stepper2
|
||||
# | * <-- probe1 probe2 --> * |
|
||||
# | |
|
||||
# | | <--- Y2 rail
|
||||
# Y1 rail -----> | |
|
||||
# | |
|
||||
# |=============================|
|
||||
# | ^ |
|
||||
# | | |
|
||||
# | X rail --/ |
|
||||
# | |
|
||||
# | * <-- probe0 probe3 --> * |
|
||||
# Z stepper0 ----> O O <---- Z stepper3
|
||||
|
||||
class QuadGantryLevel:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.retry_helper = z_tilt.RetryHelper(config,
|
||||
"Possibly Z motor numbering is wrong")
|
||||
self.max_adjust = config.getfloat("max_adjust", 4, above=0)
|
||||
self.horizontal_move_z = config.getfloat("horizontal_move_z", 5.0)
|
||||
self.probe_helper = probe.ProbePointsHelper(config, self.probe_finalize)
|
||||
if len(self.probe_helper.probe_points) != 4:
|
||||
raise config.error(
|
||||
"""{"code":"key213", "msg": "Need exactly 4 probe points for quad_gantry_level" "values": []}""")
|
||||
self.z_status = z_tilt.ZAdjustStatus(self.printer)
|
||||
self.z_helper = z_tilt.ZAdjustHelper(config, 4)
|
||||
self.gantry_corners = config.getlists('gantry_corners', parser=float,
|
||||
seps=(',', '\n'), count=2)
|
||||
if len(self.gantry_corners) < 2:
|
||||
raise config.error(
|
||||
"""{"code":"key214", "msg": "quad_gantry_level requires at least two gantry_corners" "values": []}""")
|
||||
# Register QUAD_GANTRY_LEVEL command
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.gcode.register_command(
|
||||
'QUAD_GANTRY_LEVEL', self.cmd_QUAD_GANTRY_LEVEL,
|
||||
desc=self.cmd_QUAD_GANTRY_LEVEL_help)
|
||||
cmd_QUAD_GANTRY_LEVEL_help = (
|
||||
"Conform a moving, twistable gantry to the shape of a stationary bed")
|
||||
def cmd_QUAD_GANTRY_LEVEL(self, gcmd):
|
||||
self.z_status.reset()
|
||||
self.retry_helper.start(gcmd)
|
||||
self.probe_helper.start_probe(gcmd)
|
||||
def probe_finalize(self, offsets, positions):
|
||||
# Mirror our perspective so the adjustments make sense
|
||||
# from the perspective of the gantry
|
||||
z_positions = [self.horizontal_move_z - p[2] for p in positions]
|
||||
points_message = "Gantry-relative probe points:\n%s\n" % (
|
||||
" ".join(["%s: %.6f" % (z_id, z_positions[z_id])
|
||||
for z_id in range(len(z_positions))]))
|
||||
self.gcode.respond_info(points_message)
|
||||
# Calculate slope along X axis between probe point 0 and 3
|
||||
ppx0 = [positions[0][0] + offsets[0], z_positions[0]]
|
||||
ppx3 = [positions[3][0] + offsets[0], z_positions[3]]
|
||||
slope_x_pp03 = self.linefit(ppx0, ppx3)
|
||||
# Calculate slope along X axis between probe point 1 and 2
|
||||
ppx1 = [positions[1][0] + offsets[0], z_positions[1]]
|
||||
ppx2 = [positions[2][0] + offsets[0], z_positions[2]]
|
||||
slope_x_pp12 = self.linefit(ppx1, ppx2)
|
||||
logging.info("quad_gantry_level f1: %s, f2: %s"
|
||||
% (slope_x_pp03, slope_x_pp12))
|
||||
# Calculate gantry slope along Y axis between stepper 0 and 1
|
||||
a1 = [positions[0][1] + offsets[1],
|
||||
self.plot(slope_x_pp03, self.gantry_corners[0][0])]
|
||||
a2 = [positions[1][1] + offsets[1],
|
||||
self.plot(slope_x_pp12, self.gantry_corners[0][0])]
|
||||
slope_y_s01 = self.linefit(a1, a2)
|
||||
# Calculate gantry slope along Y axis between stepper 2 and 3
|
||||
b1 = [positions[0][1] + offsets[1],
|
||||
self.plot(slope_x_pp03, self.gantry_corners[1][0])]
|
||||
b2 = [positions[1][1] + offsets[1],
|
||||
self.plot(slope_x_pp12, self.gantry_corners[1][0])]
|
||||
slope_y_s23 = self.linefit(b1, b2)
|
||||
logging.info("quad_gantry_level af: %s, bf: %s"
|
||||
% (slope_y_s01, slope_y_s23))
|
||||
# Calculate z height of each stepper
|
||||
z_height = [0,0,0,0]
|
||||
z_height[0] = self.plot(slope_y_s01, self.gantry_corners[0][1])
|
||||
z_height[1] = self.plot(slope_y_s01, self.gantry_corners[1][1])
|
||||
z_height[2] = self.plot(slope_y_s23, self.gantry_corners[1][1])
|
||||
z_height[3] = self.plot(slope_y_s23, self.gantry_corners[0][1])
|
||||
|
||||
ainfo = zip(["z","z1","z2","z3"], z_height[0:4])
|
||||
apos = " ".join(["%s: %06f" % (x) for x in ainfo])
|
||||
self.gcode.respond_info("Actuator Positions:\n" + apos)
|
||||
|
||||
z_ave = sum(z_height) / len(z_height)
|
||||
self.gcode.respond_info("Average: %0.6f" % z_ave)
|
||||
z_adjust = []
|
||||
for z in z_height:
|
||||
z_adjust.append(z_ave - z)
|
||||
|
||||
adjust_max = max(z_adjust)
|
||||
if adjust_max > self.max_adjust:
|
||||
raise self.gcode.error("""{"code":"key215", "msg": "Aborting quad_gantry_level required adjustment %0.6f is greater than max_adjust %0.6f" "values": [%0.6f,%0.6f]}"""
|
||||
% (adjust_max, self.max_adjust, adjust_max, self.max_adjust))
|
||||
|
||||
speed = self.probe_helper.get_lift_speed()
|
||||
self.z_helper.adjust_steppers(z_adjust, speed)
|
||||
return self.z_status.check_retry_result(
|
||||
self.retry_helper.check_retry(z_positions))
|
||||
|
||||
def linefit(self,p1,p2):
|
||||
if p1[1] == p2[1]:
|
||||
# Straight line
|
||||
return 0,p1[1]
|
||||
m = (p2[1] - p1[1])/(p2[0] - p1[0])
|
||||
b = p1[1] - m * p1[0]
|
||||
return m,b
|
||||
def plot(self,f,x):
|
||||
return f[0]*x + f[1]
|
||||
def get_status(self, eventtime):
|
||||
return self.z_status.get_status(eventtime)
|
||||
|
||||
def load_config(config):
|
||||
return QuadGantryLevel(config)
|
||||
@@ -0,0 +1,35 @@
|
||||
# Utility for querying the current state of adc pins
|
||||
#
|
||||
# Copyright (C) 2019 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
class QueryADC:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.adc = {}
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command("QUERY_ADC", self.cmd_QUERY_ADC,
|
||||
desc=self.cmd_QUERY_ADC_help)
|
||||
def register_adc(self, name, mcu_adc):
|
||||
self.adc[name] = mcu_adc
|
||||
cmd_QUERY_ADC_help = "Report the last value of an analog pin"
|
||||
def cmd_QUERY_ADC(self, gcmd):
|
||||
name = gcmd.get('NAME', None)
|
||||
if name not in self.adc:
|
||||
objs = ['"%s"' % (n,) for n in sorted(self.adc.keys())]
|
||||
msg = "Available ADC objects: %s" % (', '.join(objs),)
|
||||
gcmd.respond_info(msg)
|
||||
return
|
||||
value, timestamp = self.adc[name].get_last_value()
|
||||
msg = 'ADC object "%s" has value %.6f (timestamp %.3f)' % (
|
||||
name, value, timestamp)
|
||||
pullup = gcmd.get_float('PULLUP', None, above=0.)
|
||||
if pullup is not None:
|
||||
v = max(.00001, min(.99999, value))
|
||||
r = pullup * v / (1.0 - v)
|
||||
msg += "\n resistance %.3f (with %.0f pullup)" % (r, pullup)
|
||||
gcmd.respond_info(msg)
|
||||
|
||||
def load_config(config):
|
||||
return QueryADC(config)
|
||||
@@ -0,0 +1,45 @@
|
||||
# Utility for querying the current state of all endstops
|
||||
#
|
||||
# Copyright (C) 2018-2019 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
class QueryEndstops:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.endstops = []
|
||||
self.last_state = []
|
||||
# Register webhook if server is available
|
||||
webhooks = self.printer.lookup_object('webhooks')
|
||||
webhooks.register_endpoint(
|
||||
"query_endstops/status", self._handle_web_request)
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command("QUERY_ENDSTOPS", self.cmd_QUERY_ENDSTOPS,
|
||||
desc=self.cmd_QUERY_ENDSTOPS_help)
|
||||
gcode.register_command("M119", self.cmd_QUERY_ENDSTOPS)
|
||||
def register_endstop(self, mcu_endstop, name):
|
||||
self.endstops.append((mcu_endstop, name))
|
||||
def get_status(self, eventtime):
|
||||
return {'last_query': {name: value for name, value in self.last_state}}
|
||||
def _handle_web_request(self, web_request):
|
||||
gc_mutex = self.printer.lookup_object('gcode').get_mutex()
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
with gc_mutex:
|
||||
print_time = toolhead.get_last_move_time()
|
||||
self.last_state = [(name, mcu_endstop.query_endstop(print_time))
|
||||
for mcu_endstop, name in self.endstops]
|
||||
web_request.send({name: ["open", "TRIGGERED"][not not t]
|
||||
for name, t in self.last_state})
|
||||
cmd_QUERY_ENDSTOPS_help = "Report on the status of each endstop"
|
||||
def cmd_QUERY_ENDSTOPS(self, gcmd):
|
||||
# Query the endstops
|
||||
print_time = self.printer.lookup_object('toolhead').get_last_move_time()
|
||||
self.last_state = [(name, mcu_endstop.query_endstop(print_time))
|
||||
for mcu_endstop, name in self.endstops]
|
||||
# Report results
|
||||
msg = " ".join(["%s:%s" % (name, ["open", "TRIGGERED"][not not t])
|
||||
for name, t in self.last_state])
|
||||
gcmd.respond_raw(msg)
|
||||
|
||||
def load_config(config):
|
||||
return QueryEndstops(config)
|
||||
@@ -0,0 +1,271 @@
|
||||
# Code to configure miscellaneous chips
|
||||
#
|
||||
# Copyright (C) 2017-2019 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging, os
|
||||
import pins, mcu
|
||||
from . import bus
|
||||
|
||||
REPLICAPE_MAX_CURRENT = 3.84
|
||||
REPLICAPE_PCA9685_BUS = 2
|
||||
REPLICAPE_PCA9685_ADDRESS = 0x70
|
||||
REPLICAPE_PCA9685_CYCLE_TIME = .001
|
||||
PIN_MIN_TIME = 0.100
|
||||
|
||||
class pca9685_pwm:
|
||||
def __init__(self, replicape, channel, pin_type, pin_params):
|
||||
self._replicape = replicape
|
||||
self._channel = channel
|
||||
if pin_type not in ['digital_out', 'pwm']:
|
||||
raise pins.error("""{"code":"key276": "msg":"Pin type not supported on replicape", "values":[]}""")
|
||||
self._mcu = replicape.host_mcu
|
||||
self._mcu.register_config_callback(self._build_config)
|
||||
self._bus = REPLICAPE_PCA9685_BUS
|
||||
self._address = REPLICAPE_PCA9685_ADDRESS
|
||||
self._cycle_time = REPLICAPE_PCA9685_CYCLE_TIME
|
||||
self._max_duration = 2.
|
||||
self._oid = None
|
||||
self._invert = pin_params['invert']
|
||||
self._start_value = self._shutdown_value = float(self._invert)
|
||||
self._is_static = False
|
||||
self._last_clock = 0
|
||||
self._pwm_max = 0.
|
||||
self._set_cmd = None
|
||||
def get_mcu(self):
|
||||
return self._mcu
|
||||
def setup_max_duration(self, max_duration):
|
||||
self._max_duration = max_duration
|
||||
def setup_cycle_time(self, cycle_time, hardware_pwm=False):
|
||||
if hardware_pwm:
|
||||
raise pins.error("""{"code":"key216", "msg": "pca9685 does not support hardware_pwm parameter" "values": []}""")
|
||||
if cycle_time != self._cycle_time:
|
||||
logging.info("Ignoring pca9685 cycle time of %.6f (using %.6f)",
|
||||
cycle_time, self._cycle_time)
|
||||
def setup_start_value(self, start_value, shutdown_value, is_static=False):
|
||||
if is_static and start_value != shutdown_value:
|
||||
raise pins.error("""{"code":"key277": "msg":"Static pin can not have shutdown value", "values":[]}""")
|
||||
if self._invert:
|
||||
start_value = 1. - start_value
|
||||
shutdown_value = 1. - shutdown_value
|
||||
self._start_value = max(0., min(1., start_value))
|
||||
self._shutdown_value = max(0., min(1., shutdown_value))
|
||||
self._is_static = is_static
|
||||
self._replicape.note_pwm_start_value(
|
||||
self._channel, self._start_value, self._shutdown_value)
|
||||
def _build_config(self):
|
||||
self._pwm_max = self._mcu.get_constant_float("PCA9685_MAX")
|
||||
cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time)
|
||||
if self._is_static:
|
||||
self._mcu.add_config_cmd(
|
||||
"set_pca9685_out bus=%d addr=%d channel=%d"
|
||||
" cycle_ticks=%d value=%d" % (
|
||||
self._bus, self._address, self._channel,
|
||||
cycle_ticks, self._start_value * self._pwm_max))
|
||||
return
|
||||
self._mcu.request_move_queue_slot()
|
||||
self._oid = self._mcu.create_oid()
|
||||
self._mcu.add_config_cmd(
|
||||
"config_pca9685 oid=%d bus=%d addr=%d channel=%d cycle_ticks=%d"
|
||||
" value=%d default_value=%d max_duration=%d" % (
|
||||
self._oid, self._bus, self._address, self._channel, cycle_ticks,
|
||||
self._start_value * self._pwm_max,
|
||||
self._shutdown_value * self._pwm_max,
|
||||
self._mcu.seconds_to_clock(self._max_duration)))
|
||||
cmd_queue = self._mcu.alloc_command_queue()
|
||||
self._set_cmd = self._mcu.lookup_command(
|
||||
"queue_pca9685_out oid=%c clock=%u value=%hu", cq=cmd_queue)
|
||||
def set_pwm(self, print_time, value, cycle_time=None):
|
||||
clock = self._mcu.print_time_to_clock(print_time)
|
||||
if self._invert:
|
||||
value = 1. - value
|
||||
value = int(max(0., min(1., value)) * self._pwm_max + 0.5)
|
||||
self._replicape.note_pwm_enable(print_time, self._channel, value)
|
||||
self._set_cmd.send([self._oid, clock, value],
|
||||
minclock=self._last_clock, reqclock=clock)
|
||||
self._last_clock = clock
|
||||
def set_digital(self, print_time, value):
|
||||
if value:
|
||||
self.set_pwm(print_time, 1.)
|
||||
else:
|
||||
self.set_pwm(print_time, 0.)
|
||||
|
||||
class ReplicapeDACEnable:
|
||||
def __init__(self, replicape, channel, pin_type, pin_params):
|
||||
if pin_type != 'digital_out':
|
||||
raise pins.error("""{"code":"key277": "msg":"Static pin can not have shutdown value", "values":[]}""")
|
||||
if pin_params['invert']:
|
||||
raise pins.error("""{"code":"key278": "msg":"Replicape virtual enable pin can not be invertede", "values":[]}""")
|
||||
self.mcu = replicape.host_mcu
|
||||
self.value = replicape.stepper_dacs[channel]
|
||||
self.pwm = pca9685_pwm(replicape, channel, pin_type, pin_params)
|
||||
def get_mcu(self):
|
||||
return self.mcu
|
||||
def setup_max_duration(self, max_duration):
|
||||
self.pwm.setup_max_duration(max_duration)
|
||||
def set_digital(self, print_time, value):
|
||||
if value:
|
||||
self.pwm.set_pwm(print_time, self.value)
|
||||
else:
|
||||
self.pwm.set_pwm(print_time, 0.)
|
||||
|
||||
SERVO_PINS = {
|
||||
"servo0": ("/pwm0", "gpio0_30", "gpio1_18"), # P9_11, P9_14
|
||||
"servo1": ("/pwm1", "gpio3_17", "gpio1_19"), # P9_28, P9_16
|
||||
}
|
||||
|
||||
class servo_pwm:
|
||||
def __init__(self, replicape, pin_params):
|
||||
config_name = pin_params['pin']
|
||||
pwmchip = 'pwmchip0'
|
||||
if not replicape.host_mcu.is_fileoutput():
|
||||
try:
|
||||
# Determine the pwmchip number for the servo channels
|
||||
# /sys/devices/platform/ocp/48302000.epwmss/48302200.pwm/pwm
|
||||
# should be stable on the beagle bone black.
|
||||
# It contains only a "pwmchipX" directory. The entry in
|
||||
# /sys/class/pwm/ used by the Linux MCU should be a symlink
|
||||
# to this directory.
|
||||
pwmdev = os.listdir(
|
||||
'/sys/devices/platform/ocp/48302000.epwmss/48302200.pwm/pwm/')
|
||||
pwmchip = [pc for pc in pwmdev if pc.startswith('pwmchip')][0]
|
||||
except:
|
||||
raise pins.error("""{"code":"key279": "msg":"Replicape unable to determine pwmchip", "values":[]}""")
|
||||
pwm_pin, resv1, resv2 = SERVO_PINS[config_name]
|
||||
pin_params = dict(pin_params)
|
||||
pin_params['pin'] = pwmchip + pwm_pin
|
||||
# Setup actual pwm pin using linux hardware pwm on host
|
||||
self.mcu_pwm = replicape.host_mcu.setup_pin("pwm", pin_params)
|
||||
self.get_mcu = self.mcu_pwm.get_mcu
|
||||
self.setup_max_duration = self.mcu_pwm.setup_max_duration
|
||||
self.setup_start_value = self.mcu_pwm.setup_start_value
|
||||
self.set_pwm = self.mcu_pwm.set_pwm
|
||||
# Reserve pins to warn user of conflicts
|
||||
pru_mcu = replicape.mcu_pwm_enable.get_mcu()
|
||||
printer = pru_mcu.get_printer()
|
||||
ppins = printer.lookup_object('pins')
|
||||
pin_resolver = ppins.get_pin_resolver(pru_mcu.get_name())
|
||||
pin_resolver.reserve_pin(resv1, config_name)
|
||||
pin_resolver.reserve_pin(resv2, config_name)
|
||||
def setup_cycle_time(self, cycle_time, hardware_pwm=False):
|
||||
self.mcu_pwm.setup_cycle_time(cycle_time, True);
|
||||
|
||||
ReplicapeStepConfig = {
|
||||
'disable': None,
|
||||
'1': (1<<7)|(1<<5), '2': (1<<7)|(1<<5)|(1<<6), 'spread2': (1<<5),
|
||||
'4': (1<<7)|(1<<5)|(1<<4), '16': (1<<7)|(1<<5)|(1<<6)|(1<<4),
|
||||
'spread4': (1<<5)|(1<<4), 'spread16': (1<<7), 'stealth4': (1<<7)|(1<<6),
|
||||
'stealth16': 0
|
||||
}
|
||||
|
||||
class Replicape:
|
||||
def __init__(self, config):
|
||||
printer = config.get_printer()
|
||||
ppins = printer.lookup_object('pins')
|
||||
ppins.register_chip('replicape', self)
|
||||
revisions = {'B3': 'B3'}
|
||||
config.getchoice('revision', revisions)
|
||||
self.host_mcu = mcu.get_printer_mcu(printer, config.get('host_mcu'))
|
||||
# Setup enable pin
|
||||
enable_pin = config.get('enable_pin', '!gpio0_20')
|
||||
self.mcu_pwm_enable = ppins.setup_pin('digital_out', enable_pin)
|
||||
self.mcu_pwm_enable.setup_max_duration(0.)
|
||||
self.mcu_pwm_start_value = self.mcu_pwm_shutdown_value = False
|
||||
# Setup power pins
|
||||
self.pins = {
|
||||
"power_e": (pca9685_pwm, 5), "power_h": (pca9685_pwm, 3),
|
||||
"power_hotbed": (pca9685_pwm, 4),
|
||||
"power_fan0": (pca9685_pwm, 7), "power_fan1": (pca9685_pwm, 8),
|
||||
"power_fan2": (pca9685_pwm, 9), "power_fan3": (pca9685_pwm, 10) }
|
||||
self.servo_pins = {
|
||||
"servo0": 3, "servo1": 2 }
|
||||
# Setup stepper config
|
||||
self.last_stepper_time = 0.
|
||||
self.stepper_dacs = {}
|
||||
shift_registers = [1, 0, 0, 1, 1]
|
||||
for port, name in enumerate('xyzeh'):
|
||||
prefix = 'stepper_%s_' % (name,)
|
||||
sc = config.getchoice(
|
||||
prefix + 'microstep_mode', ReplicapeStepConfig, 'disable')
|
||||
if sc is None:
|
||||
continue
|
||||
sc |= shift_registers[port]
|
||||
if config.getboolean(prefix + 'chopper_off_time_high', False):
|
||||
sc |= 1<<3
|
||||
if config.getboolean(prefix + 'chopper_hysteresis_high', False):
|
||||
sc |= 1<<2
|
||||
if config.getboolean(prefix + 'chopper_blank_time_high', True):
|
||||
sc |= 1<<1
|
||||
shift_registers[port] = sc
|
||||
channel = port + 11
|
||||
cur = config.getfloat(
|
||||
prefix + 'current', above=0., maxval=REPLICAPE_MAX_CURRENT)
|
||||
self.stepper_dacs[channel] = cur / REPLICAPE_MAX_CURRENT
|
||||
self.pins[prefix + 'enable'] = (ReplicapeDACEnable, channel)
|
||||
self.enabled_channels = {ch: False for cl, ch in self.pins.values()}
|
||||
self.sr_disabled = list(reversed(shift_registers))
|
||||
if [i for i in [0, 1, 2] if 11+i in self.stepper_dacs]:
|
||||
# Enable xyz steppers
|
||||
shift_registers[0] &= ~1
|
||||
if [i for i in [3, 4] if 11+i in self.stepper_dacs]:
|
||||
# Enable eh steppers
|
||||
shift_registers[3] &= ~1
|
||||
if (config.getboolean('standstill_power_down', False)
|
||||
and self.stepper_dacs):
|
||||
shift_registers[4] &= ~1
|
||||
self.sr_enabled = list(reversed(shift_registers))
|
||||
sr_spi_bus = "spidev1.1"
|
||||
if not self.host_mcu.is_fileoutput() and os.path.exists(
|
||||
'/sys/devices/platform/ocp/481a0000.spi/spi_master/spi2'):
|
||||
sr_spi_bus = "spidev2.1"
|
||||
self.sr_spi = bus.MCU_SPI(self.host_mcu, sr_spi_bus, None, 0, 50000000)
|
||||
self.sr_spi.setup_shutdown_msg(self.sr_disabled)
|
||||
self.sr_spi.spi_send(self.sr_disabled)
|
||||
def note_pwm_start_value(self, channel, start_value, shutdown_value):
|
||||
self.mcu_pwm_start_value |= not not start_value
|
||||
self.mcu_pwm_shutdown_value |= not not shutdown_value
|
||||
self.mcu_pwm_enable.setup_start_value(
|
||||
self.mcu_pwm_start_value, self.mcu_pwm_shutdown_value)
|
||||
self.enabled_channels[channel] = not not start_value
|
||||
def note_pwm_enable(self, print_time, channel, value):
|
||||
is_enable = not not value
|
||||
if self.enabled_channels[channel] == is_enable:
|
||||
# Nothing to do
|
||||
return
|
||||
self.enabled_channels[channel] = is_enable
|
||||
# Check if need to set the pca9685 enable pin
|
||||
on_channels = [1 for c, e in self.enabled_channels.items() if e]
|
||||
if not on_channels:
|
||||
self.mcu_pwm_enable.set_digital(print_time, 0)
|
||||
elif is_enable and len(on_channels) == 1:
|
||||
self.mcu_pwm_enable.set_digital(print_time, 1)
|
||||
# Check if need to set the stepper enable lines
|
||||
if channel not in self.stepper_dacs:
|
||||
return
|
||||
on_dacs = [1 for c in self.stepper_dacs.keys()
|
||||
if self.enabled_channels[c]]
|
||||
if not on_dacs:
|
||||
sr = self.sr_disabled
|
||||
elif is_enable and len(on_dacs) == 1:
|
||||
sr = self.sr_enabled
|
||||
else:
|
||||
return
|
||||
print_time = max(print_time, self.last_stepper_time + PIN_MIN_TIME)
|
||||
clock = self.host_mcu.print_time_to_clock(print_time)
|
||||
self.sr_spi.spi_send(sr, minclock=clock, reqclock=clock)
|
||||
def setup_pin(self, pin_type, pin_params):
|
||||
pin = pin_params['pin']
|
||||
if pin in self.pins:
|
||||
pclass, channel = self.pins[pin]
|
||||
return pclass(self, channel, pin_type, pin_params)
|
||||
elif pin in self.servo_pins:
|
||||
# enable servo pins via shift registers
|
||||
index = self.servo_pins[pin]
|
||||
self.sr_enabled[index] |= 1
|
||||
self.sr_disabled[index] |= 1
|
||||
self.sr_spi.spi_send(self.sr_disabled)
|
||||
return servo_pwm(self, pin_params)
|
||||
raise pins.error("Unknown replicape pin %s" % (pin,))
|
||||
|
||||
def load_config(config):
|
||||
return Replicape(config)
|
||||
Executable
+376
@@ -0,0 +1,376 @@
|
||||
# A utility class to test resonances of the printer
|
||||
#
|
||||
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging, math, os, time
|
||||
from . import shaper_calibrate
|
||||
from subprocess import call
|
||||
|
||||
class TestAxis:
|
||||
def __init__(self, axis=None, vib_dir=None):
|
||||
if axis is None:
|
||||
self._name = "axis=%.3f,%.3f" % (vib_dir[0], vib_dir[1])
|
||||
else:
|
||||
self._name = axis
|
||||
if vib_dir is None:
|
||||
self._vib_dir = (1., 0.) if axis == 'x' else (0., 1.)
|
||||
else:
|
||||
s = math.sqrt(sum([d*d for d in vib_dir]))
|
||||
self._vib_dir = [d / s for d in vib_dir]
|
||||
def matches(self, chip_axis):
|
||||
if self._vib_dir[0] and 'x' in chip_axis:
|
||||
return True
|
||||
if self._vib_dir[1] and 'y' in chip_axis:
|
||||
return True
|
||||
return False
|
||||
def get_name(self):
|
||||
return self._name
|
||||
def get_point(self, l):
|
||||
return (self._vib_dir[0] * l, self._vib_dir[1] * l)
|
||||
|
||||
def _parse_axis(gcmd, raw_axis):
|
||||
if raw_axis is None:
|
||||
return None
|
||||
raw_axis = raw_axis.lower()
|
||||
if raw_axis in ['x', 'y']:
|
||||
return TestAxis(axis=raw_axis)
|
||||
dirs = raw_axis.split(',')
|
||||
if len(dirs) != 2:
|
||||
raise gcmd.error("""{"code": "key304", "msg": "Invalid format of axiss '%s'", "values":["%s"]}""" % (raw_axis,raw_axis))
|
||||
try:
|
||||
dir_x = float(dirs[0].strip())
|
||||
dir_y = float(dirs[1].strip())
|
||||
except:
|
||||
raise gcmd.error(
|
||||
"""{"code": "key305", "msg": "Unable to parse axis direction '%s'", "values":["%s"]}""" % (raw_axis, raw_axis))
|
||||
return TestAxis(vib_dir=(dir_x, dir_y))
|
||||
|
||||
class VibrationPulseTest:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.min_freq = config.getfloat('min_freq', 5., minval=1.)
|
||||
# Defaults are such that max_freq * accel_per_hz == 10000 (max_accel)
|
||||
self.max_freq = config.getfloat('max_freq', 10000. / 75.,
|
||||
minval=self.min_freq, maxval=200.)
|
||||
self.accel_per_hz = config.getfloat('accel_per_hz', 75., above=0.)
|
||||
self.hz_per_sec = config.getfloat('hz_per_sec', 1.,
|
||||
minval=0.1, maxval=2.)
|
||||
|
||||
self.probe_points = config.getlists('probe_points', seps=(',', '\n'),
|
||||
parser=float, count=3)
|
||||
self.low_mem = config.getboolean('low_mem', True)
|
||||
|
||||
def get_start_test_points(self):
|
||||
return self.probe_points
|
||||
def prepare_test(self, gcmd):
|
||||
self.freq_start = gcmd.get_float("FREQ_START", self.min_freq, minval=1.)
|
||||
self.freq_end = gcmd.get_float("FREQ_END", self.max_freq,
|
||||
minval=self.freq_start, maxval=200.)
|
||||
self.hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec,
|
||||
above=0., maxval=2.)
|
||||
def run_test(self, axis, gcmd):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
X, Y, Z, E = toolhead.get_position()
|
||||
sign = 1.
|
||||
freq = self.freq_start
|
||||
# Override maximum acceleration and acceleration to
|
||||
# deceleration based on the maximum test frequency
|
||||
systime = self.printer.get_reactor().monotonic()
|
||||
toolhead_info = toolhead.get_status(systime)
|
||||
old_max_accel = toolhead_info['max_accel']
|
||||
old_max_accel_to_decel = toolhead_info['max_accel_to_decel']
|
||||
max_accel = self.freq_end * self.accel_per_hz
|
||||
self.gcode.run_script_from_command(
|
||||
"SET_VELOCITY_LIMIT ACCEL=%.3f ACCEL_TO_DECEL=%.3f" % (
|
||||
max_accel, max_accel))
|
||||
input_shaper = self.printer.lookup_object('input_shaper', None)
|
||||
if input_shaper is not None and not gcmd.get_int('INPUT_SHAPING', 0):
|
||||
input_shaper.disable_shaping()
|
||||
gcmd.respond_info("Disabled [input_shaper] for resonance testing")
|
||||
else:
|
||||
input_shaper = None
|
||||
gcmd.respond_info("Testing frequency %.0f Hz" % (freq,))
|
||||
while freq <= self.freq_end + 0.000001:
|
||||
t_seg = .25 / freq
|
||||
accel = self.accel_per_hz * freq
|
||||
max_v = accel * t_seg
|
||||
toolhead.cmd_M204(self.gcode.create_gcode_command(
|
||||
"M204", "M204", {"S": accel}))
|
||||
L = .5 * accel * t_seg**2
|
||||
dX, dY = axis.get_point(L)
|
||||
nX = X + sign * dX
|
||||
nY = Y + sign * dY
|
||||
toolhead.move([nX, nY, Z, E], max_v)
|
||||
toolhead.move([X, Y, Z, E], max_v)
|
||||
sign = -sign
|
||||
old_freq = freq
|
||||
freq += 2. * t_seg * self.hz_per_sec
|
||||
if math.floor(freq) > math.floor(old_freq):
|
||||
gcmd.respond_info("Testing frequency %.0f Hz" % (freq,))
|
||||
# Restore the original acceleration values
|
||||
self.gcode.run_script_from_command(
|
||||
"SET_VELOCITY_LIMIT ACCEL=%.3f ACCEL_TO_DECEL=%.3f" % (
|
||||
old_max_accel, old_max_accel_to_decel))
|
||||
# Restore input shaper if it was disabled for resonance testing
|
||||
if input_shaper is not None:
|
||||
input_shaper.enable_shaping()
|
||||
gcmd.respond_info("Re-enabled [input_shaper]")
|
||||
|
||||
class ResonanceTester:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.move_speed = config.getfloat('move_speed', 50., above=0.)
|
||||
self.test = VibrationPulseTest(config)
|
||||
if not config.get('accel_chip_x', None):
|
||||
self.accel_chip_names = [('xy', config.get('accel_chip').strip())]
|
||||
else:
|
||||
self.accel_chip_names = [
|
||||
('x', config.get('accel_chip_x').strip()),
|
||||
('y', config.get('accel_chip_y').strip())]
|
||||
if self.accel_chip_names[0][1] == self.accel_chip_names[1][1]:
|
||||
self.accel_chip_names = [('xy', self.accel_chip_names[0][1])]
|
||||
self.max_smoothing = config.getfloat('max_smoothing', None, minval=0.05)
|
||||
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.gcode.register_command("MEASURE_AXES_NOISE",
|
||||
self.cmd_MEASURE_AXES_NOISE,
|
||||
desc=self.cmd_MEASURE_AXES_NOISE_help)
|
||||
self.gcode.register_command("TEST_RESONANCES",
|
||||
self.cmd_TEST_RESONANCES,
|
||||
desc=self.cmd_TEST_RESONANCES_help)
|
||||
self.gcode.register_command("SHAPER_CALIBRATE",
|
||||
self.cmd_SHAPER_CALIBRATE,
|
||||
desc=self.cmd_SHAPER_CALIBRATE_help)
|
||||
self.printer.register_event_handler("klippy:connect", self.connect)
|
||||
|
||||
def connect(self):
|
||||
self.accel_chips = [
|
||||
(chip_axis, self.printer.lookup_object(chip_name))
|
||||
for chip_axis, chip_name in self.accel_chip_names]
|
||||
|
||||
def _run_test(self, gcmd, axes, helper, raw_name_suffix=None,
|
||||
accel_chips=None, test_point=None):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
calibration_data = {axis: None for axis in axes}
|
||||
|
||||
self.test.prepare_test(gcmd)
|
||||
|
||||
if test_point is not None:
|
||||
test_points = [test_point]
|
||||
else:
|
||||
test_points = self.test.get_start_test_points()
|
||||
|
||||
for point in test_points:
|
||||
toolhead.manual_move(point, self.move_speed)
|
||||
if len(test_points) > 1 or test_point is not None:
|
||||
gcmd.respond_info(
|
||||
"Probing point (%.3f, %.3f, %.3f)" % tuple(point))
|
||||
for axis in axes:
|
||||
toolhead.wait_moves()
|
||||
toolhead.dwell(0.500)
|
||||
if len(axes) > 1:
|
||||
gcmd.respond_info("Testing axis %s" % axis.get_name())
|
||||
|
||||
raw_values = []
|
||||
if accel_chips is None:
|
||||
for chip_axis, chip in self.accel_chips:
|
||||
if axis.matches(chip_axis):
|
||||
aclient = chip.start_internal_client()
|
||||
raw_values.append((chip_axis, aclient, chip.name))
|
||||
else:
|
||||
for chip in accel_chips:
|
||||
aclient = chip.start_internal_client()
|
||||
raw_values.append((axis, aclient, chip.name))
|
||||
|
||||
# Generate moves
|
||||
self.test.run_test(axis, gcmd)
|
||||
for chip_axis, aclient, chip_name in raw_values:
|
||||
aclient.finish_measurements()
|
||||
if raw_name_suffix is not None:
|
||||
raw_name = self.get_filename(
|
||||
'raw_data', raw_name_suffix, axis,
|
||||
point if len(test_points) > 1 else None,
|
||||
chip_name if accel_chips is not None else None,)
|
||||
aclient.write_to_file(raw_name)
|
||||
gcmd.respond_info(
|
||||
"Writing raw accelerometer data to "
|
||||
"%s file" % (raw_name,))
|
||||
if helper is None:
|
||||
continue
|
||||
for chip_axis, aclient, chip_name in raw_values:
|
||||
if not aclient.has_valid_samples():
|
||||
raise gcmd.error(
|
||||
"""{"code":"key56", "msg":"accelerometer '%s' measured no data", "values": ["%s"]}""" % (
|
||||
chip_name, chip_name))
|
||||
if self.test.low_mem:
|
||||
new_data = helper.lowmem_process_accelerometer_data(aclient)
|
||||
else:
|
||||
new_data = helper.process_accelerometer_data(aclient)
|
||||
if calibration_data[axis] is None:
|
||||
calibration_data[axis] = new_data
|
||||
else:
|
||||
calibration_data[axis].add_data(new_data)
|
||||
return calibration_data
|
||||
cmd_TEST_RESONANCES_help = ("Runs the resonance test for a specifed axis")
|
||||
def cmd_TEST_RESONANCES(self, gcmd):
|
||||
# Parse parameters
|
||||
axis = _parse_axis(gcmd, gcmd.get("AXIS").lower())
|
||||
accel_chips = gcmd.get("CHIPS", None)
|
||||
test_point = gcmd.get("POINT", None)
|
||||
|
||||
if test_point:
|
||||
test_coords = test_point.split(',')
|
||||
if len(test_coords) != 3:
|
||||
raise gcmd.error("Invalid POINT parameter, must be 'x,y,z'")
|
||||
try:
|
||||
test_point = [float(p.strip()) for p in test_coords]
|
||||
except ValueError:
|
||||
raise gcmd.error("Invalid POINT parameter, must be 'x,y,z'"
|
||||
" where x, y and z are valid floating point numbers")
|
||||
|
||||
if accel_chips:
|
||||
parsed_chips = []
|
||||
for chip_name in accel_chips.split(','):
|
||||
if "adxl345" in chip_name:
|
||||
chip_lookup_name = chip_name.strip()
|
||||
else:
|
||||
chip_lookup_name = "adxl345 " + chip_name.strip();
|
||||
chip = self.printer.lookup_object(chip_lookup_name)
|
||||
parsed_chips.append(chip)
|
||||
|
||||
outputs = gcmd.get("OUTPUT", "resonances").lower().split(',')
|
||||
for output in outputs:
|
||||
if output not in ['resonances', 'raw_data']:
|
||||
raise gcmd.error("""{"code": "key306", "msg": "Unsupported output '%s', only 'resonances' and 'raw_data' are supported", "values":["%s"]}""" % (output, output))
|
||||
if not outputs:
|
||||
raise gcmd.error("""{"code": "key307", "msg": "No output specified, at least one of 'resonances' or 'raw_data' must be set in OUTPUT parameter", "values":[]}""")
|
||||
name_suffix = gcmd.get("NAME", time.strftime("%Y%m%d_%H%M%S"))
|
||||
if not self.is_valid_name_suffix(name_suffix):
|
||||
raise gcmd.error("""{"code":"key55", "msg":"Invalid NAME parameter", "values": []}""")
|
||||
csv_output = 'resonances' in outputs
|
||||
raw_output = 'raw_data' in outputs
|
||||
|
||||
# Setup calculation of resonances
|
||||
if csv_output:
|
||||
helper = shaper_calibrate.ShaperCalibrate(self.printer)
|
||||
else:
|
||||
helper = None
|
||||
|
||||
data = self._run_test(
|
||||
gcmd, [axis], helper,
|
||||
raw_name_suffix=name_suffix if raw_output else None,
|
||||
accel_chips=parsed_chips if accel_chips else None,
|
||||
test_point=test_point)[axis]
|
||||
if csv_output:
|
||||
csv_name = self.save_calibration_data('resonances', name_suffix,
|
||||
helper, axis, data,
|
||||
point=test_point)
|
||||
gcmd.respond_info(
|
||||
"Resonances data written to %s file" % (csv_name,))
|
||||
cmd_SHAPER_CALIBRATE_help = (
|
||||
"Simular to TEST_RESONANCES but suggest input shaper config")
|
||||
def cmd_SHAPER_CALIBRATE(self, gcmd):
|
||||
# Parse parameters
|
||||
axis = gcmd.get("AXIS", None)
|
||||
if not axis:
|
||||
calibrate_axes = [TestAxis('x'), TestAxis('y')]
|
||||
elif axis.lower() not in 'xy':
|
||||
raise gcmd.error("Unsupported axis '%s'" % (axis,))
|
||||
else:
|
||||
calibrate_axes = [TestAxis(axis.lower())]
|
||||
|
||||
max_smoothing = gcmd.get_float(
|
||||
"MAX_SMOOTHING", self.max_smoothing, minval=0.05)
|
||||
|
||||
name_suffix = gcmd.get("NAME", time.strftime("%Y%m%d_%H%M%S"))
|
||||
if not self.is_valid_name_suffix(name_suffix):
|
||||
raise gcmd.error("Invalid NAME parameter")
|
||||
|
||||
# Setup shaper calibration
|
||||
helper = shaper_calibrate.ShaperCalibrate(self.printer)
|
||||
|
||||
calibration_data = self._run_test(gcmd, calibrate_axes, helper)
|
||||
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
for axis in calibrate_axes:
|
||||
axis_name = axis.get_name()
|
||||
gcmd.respond_info(
|
||||
"Calculating the best input shaper parameters for %s axis"
|
||||
% (axis_name,))
|
||||
calibration_data[axis].normalize_to_frequencies()
|
||||
best_shaper, all_shapers = helper.find_best_shaper(
|
||||
calibration_data[axis], max_smoothing, gcmd.respond_info)
|
||||
gcmd.respond_info(
|
||||
"Recommended shaper_type_%s = %s, shaper_freq_%s = %.1f Hz"
|
||||
% (axis_name, best_shaper.name,
|
||||
axis_name, best_shaper.freq))
|
||||
helper.save_params(configfile, axis_name,
|
||||
best_shaper.name, best_shaper.freq)
|
||||
csv_name = self.save_calibration_data(
|
||||
'calibration_data', name_suffix, helper, axis,
|
||||
calibration_data[axis], all_shapers)
|
||||
gcmd.respond_info(
|
||||
"Shaper calibration data written to %s file" % (csv_name,))
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.run_script_from_command("CXSAVE_CONFIG")
|
||||
call("sync", shell=True)
|
||||
input_shaper = self.printer.lookup_object("input_shaper", None)
|
||||
if not input_shaper:
|
||||
config = configfile.read_main_config()
|
||||
self.printer.reload_object(config, "input_shaper")
|
||||
gcode.run_script_from_command("UPDATE_INPUT_SHAPER")
|
||||
input_shaper = self.printer.lookup_object("input_shaper", None)
|
||||
input_shaper.enable_shaping()
|
||||
gcmd.respond_info(
|
||||
"The SAVE_CONFIG command will update the printer config file\n"
|
||||
"with these parameters and restart the printer.")
|
||||
cmd_MEASURE_AXES_NOISE_help = (
|
||||
"Measures noise of all enabled accelerometer chips")
|
||||
def cmd_MEASURE_AXES_NOISE(self, gcmd):
|
||||
meas_time = gcmd.get_float("MEAS_TIME", 2.)
|
||||
raw_values = [(chip_axis, chip.start_internal_client())
|
||||
for chip_axis, chip in self.accel_chips]
|
||||
self.printer.lookup_object('toolhead').dwell(meas_time)
|
||||
for chip_axis, aclient in raw_values:
|
||||
aclient.finish_measurements()
|
||||
helper = shaper_calibrate.ShaperCalibrate(self.printer)
|
||||
for chip_axis, aclient in raw_values:
|
||||
if not aclient.has_valid_samples():
|
||||
raise gcmd.error(
|
||||
"%s-axis accelerometer measured no data" % (
|
||||
chip_axis,))
|
||||
data = helper.process_accelerometer_data(aclient)
|
||||
vx = data.psd_x.mean()
|
||||
vy = data.psd_y.mean()
|
||||
vz = data.psd_z.mean()
|
||||
gcmd.respond_info("Axes noise for %s-axis accelerometer: "
|
||||
"%.6f (x), %.6f (y), %.6f (z)" % (
|
||||
chip_axis, vx, vy, vz))
|
||||
|
||||
def is_valid_name_suffix(self, name_suffix):
|
||||
return name_suffix.replace('-', '').replace('_', '').isalnum()
|
||||
|
||||
def get_filename(self, base, name_suffix, axis=None,
|
||||
point=None, chip_name=None):
|
||||
name = base
|
||||
if axis:
|
||||
name += '_' + axis.get_name()
|
||||
if chip_name:
|
||||
name += '_' + chip_name.replace(" ", "_")
|
||||
if point:
|
||||
name += "_%.3f_%.3f_%.3f" % (point[0], point[1], point[2])
|
||||
name += '_' + name_suffix
|
||||
return os.path.join("/tmp", name + ".csv")
|
||||
|
||||
def save_calibration_data(self, base_name, name_suffix, shaper_calibrate,
|
||||
axis, calibration_data,
|
||||
all_shapers=None, point=None):
|
||||
output = self.get_filename(base_name, name_suffix, axis, point)
|
||||
shaper_calibrate.save_calibration_data(output, calibration_data,
|
||||
all_shapers)
|
||||
return output
|
||||
|
||||
def load_config(config):
|
||||
return ResonanceTester(config)
|
||||
@@ -0,0 +1,55 @@
|
||||
# Add 'RESPOND' and 'M118' commands for sending messages to the host
|
||||
#
|
||||
# Copyright (C) 2018 Alec Plumb <alec@etherwalker.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
respond_types = {
|
||||
'echo': 'echo:',
|
||||
'command': '//',
|
||||
'error' : '!!',
|
||||
}
|
||||
|
||||
respond_types_no_space = {
|
||||
'echo_no_space': 'echo:',
|
||||
}
|
||||
|
||||
class HostResponder:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.reactor = self.printer.get_reactor()
|
||||
self.default_prefix = config.getchoice('default_type', respond_types,
|
||||
'echo')
|
||||
self.default_prefix = config.get('default_prefix', self.default_prefix)
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command('M118', self.cmd_M118, True)
|
||||
gcode.register_command('RESPOND', self.cmd_RESPOND, True,
|
||||
desc=self.cmd_RESPOND_help)
|
||||
def cmd_M118(self, gcmd):
|
||||
msg = gcmd.get_raw_command_parameters()
|
||||
gcmd.respond_raw("%s %s" % (self.default_prefix, msg))
|
||||
cmd_RESPOND_help = ("Echo the message prepended with a prefix")
|
||||
def cmd_RESPOND(self, gcmd):
|
||||
no_space = False
|
||||
respond_type = gcmd.get('TYPE', None)
|
||||
prefix = self.default_prefix
|
||||
if(respond_type != None):
|
||||
respond_type = respond_type.lower()
|
||||
if(respond_type in respond_types):
|
||||
prefix = respond_types[respond_type]
|
||||
elif(respond_type in respond_types_no_space):
|
||||
prefix = respond_types_no_space[respond_type]
|
||||
no_space = True
|
||||
else:
|
||||
raise gcmd.error(
|
||||
"""{"code": "key309", "msg": "RESPOND TYPE '%s' is invalid. Must be one of 'echo', 'command', or 'error'", "values":["%s"]}""" % (
|
||||
respond_type, respond_type))
|
||||
prefix = gcmd.get('PREFIX', prefix)
|
||||
msg = gcmd.get('MSG', '')
|
||||
if(no_space):
|
||||
gcmd.respond_raw("%s%s" % (prefix, msg))
|
||||
else:
|
||||
gcmd.respond_raw("%s %s" % (prefix, msg))
|
||||
|
||||
def load_config(config):
|
||||
return HostResponder(config)
|
||||
@@ -0,0 +1,90 @@
|
||||
# Perform Z Homing at specific XY coordinates.
|
||||
#
|
||||
# Copyright (C) 2019 Florian Heilmann <Florian.Heilmann@gmx.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
class SafeZHoming:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
x_pos, y_pos = config.getfloatlist("home_xy_position", count=2)
|
||||
self.home_x_pos, self.home_y_pos = x_pos, y_pos
|
||||
self.z_hop = config.getfloat("z_hop", default=0.0)
|
||||
self.z_hop_speed = config.getfloat('z_hop_speed', 15., above=0.)
|
||||
zconfig = config.getsection('stepper_z')
|
||||
self.max_z = zconfig.getfloat('position_max', note_valid=False)
|
||||
self.speed = config.getfloat('speed', 50.0, above=0.)
|
||||
self.move_to_previous = config.getboolean('move_to_previous', 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)
|
||||
|
||||
if config.has_section("homing_override"):
|
||||
raise config.error("""{"code":"key106", "msg": "homing_override and safe_z_homing cannot be used simultaneously", "values": []}""")
|
||||
|
||||
def cmd_G28(self, gcmd):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
|
||||
# Perform Z Hop if necessary
|
||||
if self.z_hop != 0.0:
|
||||
# Check if Z axis is homed and its last known position
|
||||
curtime = self.printer.get_reactor().monotonic()
|
||||
kin_status = toolhead.get_kinematics().get_status(curtime)
|
||||
pos = toolhead.get_position()
|
||||
|
||||
if 'z' not in kin_status['homed_axes']:
|
||||
# Always perform the z_hop if the Z axis is not homed
|
||||
pos[2] = 0
|
||||
toolhead.set_position(pos, homing_axes=[2])
|
||||
toolhead.manual_move([None, None, self.z_hop],
|
||||
self.z_hop_speed)
|
||||
if hasattr(toolhead.get_kinematics(), "note_z_not_homed"):
|
||||
toolhead.get_kinematics().note_z_not_homed()
|
||||
elif pos[2] < self.z_hop:
|
||||
# If the Z axis is homed, and below z_hop, lift it to z_hop
|
||||
toolhead.manual_move([None, None, self.z_hop],
|
||||
self.z_hop_speed)
|
||||
|
||||
# Determine which axes we need to home
|
||||
need_x, need_y, need_z = [gcmd.get(axis, None) is not None
|
||||
for axis in "XYZ"]
|
||||
if not need_x and not need_y and not need_z:
|
||||
need_x = need_y = need_z = True
|
||||
|
||||
# Home XY axes if necessary
|
||||
new_params = {}
|
||||
if need_x:
|
||||
new_params['X'] = '0'
|
||||
if need_y:
|
||||
new_params['Y'] = '0'
|
||||
if new_params:
|
||||
g28_gcmd = self.gcode.create_gcode_command("G28", "G28", new_params)
|
||||
self.prev_G28(g28_gcmd)
|
||||
|
||||
# Home Z axis if necessary
|
||||
if need_z:
|
||||
# Throw an error if X or Y are not homed
|
||||
curtime = self.printer.get_reactor().monotonic()
|
||||
kin_status = toolhead.get_kinematics().get_status(curtime)
|
||||
if ('x' not in kin_status['homed_axes'] or
|
||||
'y' not in kin_status['homed_axes']):
|
||||
raise gcmd.error("""{"code": "key600", "msg":"Must home X and Y axes first", "values": []}""")
|
||||
# Move to safe XY homing position
|
||||
prevpos = toolhead.get_position()
|
||||
toolhead.manual_move([self.home_x_pos, self.home_y_pos], self.speed)
|
||||
# Home Z
|
||||
g28_gcmd = self.gcode.create_gcode_command("G28", "G28", {'Z': '0'})
|
||||
self.prev_G28(g28_gcmd)
|
||||
# Perform Z Hop again for pressure-based probes
|
||||
if self.z_hop:
|
||||
pos = toolhead.get_position()
|
||||
if pos[2] < self.z_hop:
|
||||
toolhead.manual_move([None, None, self.z_hop],
|
||||
self.z_hop_speed)
|
||||
# Move XY back to previous positions
|
||||
if self.move_to_previous:
|
||||
toolhead.manual_move(prevpos[:2], self.speed)
|
||||
|
||||
def load_config(config):
|
||||
return SafeZHoming(config)
|
||||
@@ -0,0 +1,41 @@
|
||||
# SAMD Sercom configuration
|
||||
#
|
||||
# Copyright (C) 2019 Florian Heilmann <Florian.Heilmann@gmx.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
class SamdSERCOM:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
|
||||
self.sercom = config.get("sercom")
|
||||
self.tx_pin = config.get("tx_pin")
|
||||
self.rx_pin = config.get("rx_pin", None)
|
||||
self.clk_pin = config.get("clk_pin")
|
||||
|
||||
ppins = self.printer.lookup_object("pins")
|
||||
tx_pin_params = ppins.lookup_pin(self.tx_pin)
|
||||
self.mcu = tx_pin_params['chip']
|
||||
self.mcu.add_config_cmd(
|
||||
"set_sercom_pin bus=%s sercom_pin_type=tx pin=%s" % (
|
||||
self.sercom, tx_pin_params['pin']))
|
||||
|
||||
clk_pin_params = ppins.lookup_pin(self.clk_pin)
|
||||
if self.mcu is not clk_pin_params['chip']:
|
||||
raise ppins.error("%s: SERCOM pins must be on same mcu" % (
|
||||
config.get_name(),))
|
||||
self.mcu.add_config_cmd(
|
||||
"set_sercom_pin bus=%s sercom_pin_type=clk pin=%s" % (
|
||||
self.sercom, clk_pin_params['pin']))
|
||||
|
||||
if self.rx_pin:
|
||||
rx_pin_params = ppins.lookup_pin(self.rx_pin)
|
||||
if self.mcu is not rx_pin_params['chip']:
|
||||
raise ppins.error("%s: SERCOM pins must be on same mcu" % (
|
||||
config.get_name(),))
|
||||
self.mcu.add_config_cmd(
|
||||
"set_sercom_pin bus=%s sercom_pin_type=rx pin=%s" % (
|
||||
self.sercom, rx_pin_params['pin']))
|
||||
|
||||
def load_config_prefix(config):
|
||||
return SamdSERCOM(config)
|
||||
@@ -0,0 +1,64 @@
|
||||
# Save arbitrary variables so that values can be kept across restarts.
|
||||
#
|
||||
# Copyright (C) 2020 Dushyant Ahuja <dusht.ahuja@gmail.com>
|
||||
# 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, ast, configparser
|
||||
|
||||
class SaveVariables:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.filename = os.path.expanduser(config.get('filename'))
|
||||
self.allVariables = {}
|
||||
try:
|
||||
if not os.path.exists(self.filename):
|
||||
open(self.filename, "w").close()
|
||||
self.loadVariables()
|
||||
except self.printer.command_error as e:
|
||||
raise config.error(str(e))
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command('SAVE_VARIABLE', self.cmd_SAVE_VARIABLE,
|
||||
desc=self.cmd_SAVE_VARIABLE_help)
|
||||
def loadVariables(self):
|
||||
allvars = {}
|
||||
varfile = configparser.ConfigParser()
|
||||
try:
|
||||
varfile.read(self.filename)
|
||||
if varfile.has_section('Variables'):
|
||||
for name, val in varfile.items('Variables'):
|
||||
allvars[name] = ast.literal_eval(val)
|
||||
except:
|
||||
msg = """{"code": "key284", "msg": ""Unable to parse existing variable file", "values": []}"""
|
||||
logging.exception(msg)
|
||||
raise self.printer.command_error(msg)
|
||||
self.allVariables = allvars
|
||||
cmd_SAVE_VARIABLE_help = "Save arbitrary variables to disk"
|
||||
def cmd_SAVE_VARIABLE(self, gcmd):
|
||||
varname = gcmd.get('VARIABLE')
|
||||
value = gcmd.get('VALUE')
|
||||
try:
|
||||
value = ast.literal_eval(value)
|
||||
except ValueError as e:
|
||||
raise gcmd.error("""{"code": "key285", "msg": "Unable to parse '%s' as a literal", "values": ["%s"]}""" % (value, value))
|
||||
newvars = dict(self.allVariables)
|
||||
newvars[varname] = value
|
||||
# Write file
|
||||
varfile = configparser.ConfigParser()
|
||||
varfile.add_section('Variables')
|
||||
for name, val in sorted(newvars.items()):
|
||||
varfile.set('Variables', name, repr(val))
|
||||
try:
|
||||
f = open(self.filename, "w")
|
||||
varfile.write(f)
|
||||
f.close()
|
||||
except:
|
||||
msg = """{"code": "key286", "msg": "Unable to save variable", "values": []}"""
|
||||
logging.exception(msg)
|
||||
raise gcmd.error(msg)
|
||||
self.loadVariables()
|
||||
def get_status(self, eventtime):
|
||||
return {'variables': self.allVariables}
|
||||
|
||||
def load_config(config):
|
||||
return SaveVariables(config)
|
||||
@@ -0,0 +1,73 @@
|
||||
# Sdcard file looping support
|
||||
#
|
||||
# Copyright (C) 2021 Jason S. McMullan <jason.mcmullan@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
import logging
|
||||
|
||||
class SDCardLoop:
|
||||
def __init__(self, config):
|
||||
printer = config.get_printer()
|
||||
self.sdcard = printer.load_object(config, "virtual_sdcard")
|
||||
self.gcode = printer.lookup_object('gcode')
|
||||
self.gcode.register_command(
|
||||
"SDCARD_LOOP_BEGIN", self.cmd_SDCARD_LOOP_BEGIN,
|
||||
desc=self.cmd_SDCARD_LOOP_BEGIN_help)
|
||||
self.gcode.register_command(
|
||||
"SDCARD_LOOP_END", self.cmd_SDCARD_LOOP_END,
|
||||
desc=self.cmd_SDCARD_LOOP_END_help)
|
||||
self.gcode.register_command(
|
||||
"SDCARD_LOOP_DESIST", self.cmd_SDCARD_LOOP_DESIST,
|
||||
desc=self.cmd_SDCARD_LOOP_DESIST_help)
|
||||
self.loop_stack = []
|
||||
cmd_SDCARD_LOOP_BEGIN_help = "Begins a looped section in the SD file."
|
||||
def cmd_SDCARD_LOOP_BEGIN(self, gcmd):
|
||||
count = gcmd.get_int("COUNT", minval=0)
|
||||
if not self.loop_begin(count):
|
||||
raise gcmd.error("""{"code":"key176", "msg": "Only permitted in SD file.", "values": []}""")
|
||||
cmd_SDCARD_LOOP_END_help = "Ends a looped section in the SD file."
|
||||
def cmd_SDCARD_LOOP_END(self, gcmd):
|
||||
if not self.loop_end():
|
||||
raise gcmd.error("""{"code":"key176", "msg": "Only permitted in SD file.", "values": []}""")
|
||||
cmd_SDCARD_LOOP_DESIST_help = "Stops iterating the current loop stack."
|
||||
def cmd_SDCARD_LOOP_DESIST(self, gcmd):
|
||||
if not self.loop_desist():
|
||||
raise gcmd.error("""{"code":"key177", "msg": "Only permitted outside of a SD file..", "values": []}""")
|
||||
def loop_begin(self, count):
|
||||
if not self.sdcard.is_cmd_from_sd():
|
||||
# Can only run inside of an SD file
|
||||
return False
|
||||
self.loop_stack.append((count, self.sdcard.get_file_position()))
|
||||
return True
|
||||
def loop_end(self):
|
||||
if not self.sdcard.is_cmd_from_sd():
|
||||
# Can only run inside of an SD file
|
||||
return False
|
||||
# If the stack is empty, no need to skip back
|
||||
if len(self.loop_stack) == 0:
|
||||
return True
|
||||
# Get iteration count and return position
|
||||
count, position = self.loop_stack.pop()
|
||||
if count == 0: # Infinite loop
|
||||
self.sdcard.set_file_position(position)
|
||||
self.loop_stack.append((0, position))
|
||||
elif count == 1: # Last repeat
|
||||
# Nothing to do
|
||||
pass
|
||||
else:
|
||||
# At the next opportunity, seek back to the start of the loop
|
||||
self.sdcard.set_file_position(position)
|
||||
# Decrement the count by 1, and add the position back to the stack
|
||||
self.loop_stack.append((count - 1, position))
|
||||
return True
|
||||
def loop_desist(self):
|
||||
if self.sdcard.is_cmd_from_sd():
|
||||
# Can only run outside of an SD file
|
||||
return False
|
||||
logging.info("Desisting existing SD loops")
|
||||
self.loop_stack = []
|
||||
return True
|
||||
|
||||
def load_config(config):
|
||||
return SDCardLoop(config)
|
||||
Executable
+473
@@ -0,0 +1,473 @@
|
||||
# Automatic calibration of input shapers
|
||||
#
|
||||
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import collections, importlib, logging, math, multiprocessing, traceback, os
|
||||
import time, subprocess, shlex
|
||||
from multiprocessing import shared_memory
|
||||
shaper_defs = importlib.import_module('.shaper_defs', 'extras')
|
||||
|
||||
MIN_FREQ = 5.
|
||||
MAX_FREQ = 200.
|
||||
WINDOW_T_SEC = 0.5
|
||||
MAX_SHAPER_FREQ = 150.
|
||||
|
||||
TEST_DAMPING_RATIOS=[0.075, 0.1, 0.15]
|
||||
|
||||
AUTOTUNE_SHAPERS = ['zv', 'mzv', 'ei', '2hump_ei', '3hump_ei']
|
||||
|
||||
######################################################################
|
||||
# Frequency response calculation and shaper auto-tuning
|
||||
######################################################################
|
||||
|
||||
def exec_cmd(conn, method):
|
||||
try:
|
||||
val = os.nice(10)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(shlex.split(method), stdout=subprocess.PIPE)
|
||||
output = process.communicate()[0]
|
||||
retcode = process.poll()
|
||||
except:
|
||||
retcode = -1
|
||||
conn.send((True, retcode))
|
||||
conn.close()
|
||||
return
|
||||
if retcode is 0:
|
||||
conn.send((False, retcode))
|
||||
else:
|
||||
conn.send((True, retcode))
|
||||
conn.close()
|
||||
|
||||
class CalibrationData:
|
||||
def __init__(self, freq_bins, psd_sum, psd_x, psd_y, psd_z):
|
||||
self.freq_bins = freq_bins
|
||||
self.psd_sum = psd_sum
|
||||
self.psd_x = psd_x
|
||||
self.psd_y = psd_y
|
||||
self.psd_z = psd_z
|
||||
self._psd_list = [self.psd_sum, self.psd_x, self.psd_y, self.psd_z]
|
||||
self._psd_map = {'x': self.psd_x, 'y': self.psd_y, 'z': self.psd_z,
|
||||
'all': self.psd_sum}
|
||||
self.data_sets = 1
|
||||
def add_data(self, other):
|
||||
np = self.numpy
|
||||
joined_data_sets = self.data_sets + other.data_sets
|
||||
for psd, other_psd in zip(self._psd_list, other._psd_list):
|
||||
# `other` data may be defined at different frequency bins,
|
||||
# interpolating to fix that.
|
||||
other_normalized = other.data_sets * np.interp(
|
||||
self.freq_bins, other.freq_bins, other_psd)
|
||||
psd *= self.data_sets
|
||||
psd[:] = (psd + other_normalized) * (1. / joined_data_sets)
|
||||
self.data_sets = joined_data_sets
|
||||
def set_numpy(self, numpy):
|
||||
self.numpy = numpy
|
||||
def normalize_to_frequencies(self):
|
||||
for psd in self._psd_list:
|
||||
# Avoid division by zero errors
|
||||
psd /= self.freq_bins + .1
|
||||
# Remove low-frequency noise
|
||||
psd[self.freq_bins < MIN_FREQ] = 0.
|
||||
def get_psd(self, axis='all'):
|
||||
return self._psd_map[axis]
|
||||
|
||||
|
||||
CalibrationResult = collections.namedtuple(
|
||||
'CalibrationResult',
|
||||
('name', 'freq', 'vals', 'vibrs', 'smoothing', 'score', 'max_accel'))
|
||||
|
||||
class ShaperCalibrate:
|
||||
def __init__(self, printer):
|
||||
self.printer = printer
|
||||
self.error = printer.command_error if printer else Exception
|
||||
self.autotune_shapers = ['zv', 'mzv', 'ei', '2hump_ei', '3hump_ei']
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
gcode_macro_path = '/usr/data/printer_data/config/gcode_macro.cfg'
|
||||
gconfig = None
|
||||
try:
|
||||
gconfig = configfile.read_config(gcode_macro_path)
|
||||
if gconfig and gconfig.has_section('gcode_macro AUTOTUNE_SHAPERS'):
|
||||
AUTOTUNE_SHAPERS = gconfig.getsection('gcode_macro AUTOTUNE_SHAPERS')
|
||||
self.autotune_shapers = list(map(lambda x: x.replace("'", "") , AUTOTUNE_SHAPERS.getlist('variable_autotune_shapers', ['zv', 'mzv', 'ei', '2hump_ei', '3hump_ei'])))
|
||||
except Exception as err:
|
||||
logging.error("gcode_macro_path: %s, configfile.read_config error:%s" % (gcode_macro_path, err))
|
||||
try:
|
||||
self.numpy = importlib.import_module('numpy')
|
||||
except ImportError:
|
||||
raise self.error(
|
||||
"Failed to import `numpy` module, make sure it was "
|
||||
"installed via `~/klippy-env/bin/pip install` (refer to "
|
||||
"docs/Measuring_Resonances.md for more details).")
|
||||
|
||||
def background_process_exec(self, method, args):
|
||||
if self.printer is None:
|
||||
return method(*args)
|
||||
import queuelogger
|
||||
parent_conn, child_conn = multiprocessing.Pipe()
|
||||
def wrapper():
|
||||
try:
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
gcode.respond_info("current nice: %d" % os.nice(0), log=False)
|
||||
val = os.nice(10)
|
||||
gcode.respond_info("process id: %d, current nice: %d" % (os.getpid(), val), log=False)
|
||||
except:
|
||||
gcode.respond_info("nice process failed", log=False)
|
||||
pass
|
||||
queuelogger.clear_bg_logging()
|
||||
try:
|
||||
res = method(*args)
|
||||
except:
|
||||
child_conn.send((True, traceback.format_exc()))
|
||||
child_conn.close()
|
||||
return
|
||||
child_conn.send((False, res))
|
||||
child_conn.close()
|
||||
# Start a process to perform the calculation
|
||||
calc_proc = multiprocessing.Process(target=wrapper)
|
||||
calc_proc.daemon = True
|
||||
calc_proc.start()
|
||||
# Wait for the process to finish
|
||||
reactor = self.printer.get_reactor()
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
eventtime = last_report_time = reactor.monotonic()
|
||||
while calc_proc.is_alive():
|
||||
if eventtime > last_report_time + 5.:
|
||||
last_report_time = eventtime
|
||||
gcode.respond_info("Wait for calculations..", log=False)
|
||||
eventtime = reactor.pause(eventtime + .1)
|
||||
# Return results
|
||||
is_err, res = parent_conn.recv()
|
||||
if is_err:
|
||||
raise self.error("""{"code": "key312", "msg": "Error in remote calculation: %s", "values":["%s"]}""" % (res,res))
|
||||
calc_proc.join()
|
||||
parent_conn.close()
|
||||
return res
|
||||
|
||||
def _split_into_windows(self, x, window_size, overlap):
|
||||
# Memory-efficient algorithm to split an input 'x' into a series
|
||||
# of overlapping windows
|
||||
step_between_windows = window_size - overlap
|
||||
n_windows = (x.shape[-1] - overlap) // step_between_windows
|
||||
shape = (window_size, n_windows)
|
||||
strides = (x.strides[-1], step_between_windows * x.strides[-1])
|
||||
return self.numpy.lib.stride_tricks.as_strided(
|
||||
x, shape=shape, strides=strides, writeable=False)
|
||||
|
||||
def _psd(self, x, fs, nfft):
|
||||
# Calculate power spectral density (PSD) using Welch's algorithm
|
||||
np = self.numpy
|
||||
window = np.kaiser(nfft, 6.)
|
||||
# Compensation for windowing loss
|
||||
scale = 1.0 / (window**2).sum()
|
||||
|
||||
# Split into overlapping windows of size nfft
|
||||
overlap = nfft // 2
|
||||
x = self._split_into_windows(x, nfft, overlap)
|
||||
|
||||
# First detrend, then apply windowing function
|
||||
x = window[:, None] * (x - np.mean(x, axis=0))
|
||||
|
||||
# Calculate frequency response for each window using FFT
|
||||
result = np.fft.rfft(x, n=nfft, axis=0)
|
||||
result = np.conjugate(result) * result
|
||||
result *= scale / fs
|
||||
# For one-sided FFT output the response must be doubled, except
|
||||
# the last point for unpaired Nyquist frequency (assuming even nfft)
|
||||
# and the 'DC' term (0 Hz)
|
||||
result[1:-1,:] *= 2.
|
||||
|
||||
# Welch's algorithm: average response over windows
|
||||
psd = result.real.mean(axis=-1)
|
||||
|
||||
# Calculate the frequency bins
|
||||
freqs = np.fft.rfftfreq(nfft, 1. / fs)
|
||||
return freqs, psd
|
||||
|
||||
def calc_freq_response(self, raw_values):
|
||||
np = self.numpy
|
||||
if raw_values is None:
|
||||
return None
|
||||
if isinstance(raw_values, np.ndarray):
|
||||
data = raw_values
|
||||
else:
|
||||
samples = raw_values.get_samples()
|
||||
if not samples:
|
||||
return None
|
||||
data = np.array(samples)
|
||||
|
||||
N = data.shape[0]
|
||||
T = data[-1,0] - data[0,0]
|
||||
SAMPLING_FREQ = N / T
|
||||
# Round up to the nearest power of 2 for faster FFT
|
||||
M = 1 << int(SAMPLING_FREQ * WINDOW_T_SEC - 1).bit_length()
|
||||
if N <= M:
|
||||
return None
|
||||
|
||||
# Calculate PSD (power spectral density) of vibrations per
|
||||
# frequency bins (the same bins for X, Y, and Z)
|
||||
fx, px = self._psd(data[:,1], SAMPLING_FREQ, M)
|
||||
fy, py = self._psd(data[:,2], SAMPLING_FREQ, M)
|
||||
fz, pz = self._psd(data[:,3], SAMPLING_FREQ, M)
|
||||
return CalibrationData(fx, px+py+pz, px, py, pz)
|
||||
|
||||
def process_accelerometer_data(self, data):
|
||||
calibration_data = self.background_process_exec(
|
||||
self.calc_freq_response, (data,))
|
||||
if calibration_data is None:
|
||||
raise self.error(
|
||||
"""{"code": "key313", "msg": "Internal error processing accelerometer data %s", "values":["%s"]}""" % (data,data))
|
||||
calibration_data.set_numpy(self.numpy)
|
||||
return calibration_data
|
||||
|
||||
def lowmem_background_process_exec(self, method):
|
||||
if self.printer is None:
|
||||
return None
|
||||
|
||||
ctx = multiprocessing.get_context('spawn')
|
||||
parent_conn, child_conn = multiprocessing.Pipe()
|
||||
|
||||
# Start a process to perform the calculation
|
||||
calc_proc = ctx.Process(target=exec_cmd, args=(child_conn, method))
|
||||
calc_proc.daemon = True
|
||||
calc_proc.start()
|
||||
# Wait for the process to finish
|
||||
reactor = self.printer.get_reactor()
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
eventtime = last_report_time = reactor.monotonic()
|
||||
while calc_proc.is_alive():
|
||||
if eventtime > last_report_time + 5.:
|
||||
last_report_time = eventtime
|
||||
gcode.respond_info("Wait for calculations..")
|
||||
eventtime = reactor.pause(eventtime + .1)
|
||||
# Return results
|
||||
is_err, res = parent_conn.recv()
|
||||
if is_err:
|
||||
raise self.error("""{"code": "key312", "msg": "Error in remote calculation: %s", "values":["%s"]}""" % (res,res))
|
||||
calc_proc.join()
|
||||
parent_conn.close()
|
||||
return res
|
||||
|
||||
def copy_samples_to_shared_memory(self, data):
|
||||
data.get_samples_to_shared_mem()
|
||||
|
||||
def read_results_from_shared_memory(self, name):
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
try:
|
||||
shm = shared_memory.SharedMemory(name)
|
||||
except:
|
||||
gcode.respond_info("open shared memory %s fail!" % (name))
|
||||
return None
|
||||
|
||||
np = self.numpy
|
||||
array = np.ndarray((shm.size // 8, ), dtype = np.float64, buffer = shm.buf, offset = 0)
|
||||
shm.unlink()
|
||||
|
||||
return array.copy()
|
||||
|
||||
def lowmem_process_accelerometer_data(self, data):
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
|
||||
self.copy_samples_to_shared_memory(data)
|
||||
|
||||
# call c++ program and return result by shared memory
|
||||
ret = self.lowmem_background_process_exec("/usr/bin/calc_psd")
|
||||
gcode.respond_info("calc_freq_response return (%d)" % (ret))
|
||||
|
||||
if ret is 0:
|
||||
fx = self.read_results_from_shared_memory("psm_freq")
|
||||
px = self.read_results_from_shared_memory("psm_px")
|
||||
py = self.read_results_from_shared_memory("psm_py")
|
||||
pz = self.read_results_from_shared_memory("psm_pz")
|
||||
|
||||
calibration_data = CalibrationData(fx, px+py+pz, px, py, pz)
|
||||
else:
|
||||
calibration_data = None
|
||||
|
||||
if calibration_data is None:
|
||||
raise self.error(
|
||||
"""{"code": "key313", "msg": "Internal error processing accelerometer data %s", "values":["%s"]}""" % (data,data))
|
||||
calibration_data.set_numpy(self.numpy)
|
||||
return calibration_data
|
||||
|
||||
def _estimate_shaper(self, shaper, test_damping_ratio, test_freqs):
|
||||
np = self.numpy
|
||||
|
||||
A, T = np.array(shaper[0]), np.array(shaper[1])
|
||||
inv_D = 1. / A.sum()
|
||||
|
||||
omega = 2. * math.pi * test_freqs
|
||||
damping = test_damping_ratio * omega
|
||||
omega_d = omega * math.sqrt(1. - test_damping_ratio**2)
|
||||
W = A * np.exp(np.outer(-damping, (T[-1] - T)))
|
||||
S = W * np.sin(np.outer(omega_d, T))
|
||||
C = W * np.cos(np.outer(omega_d, T))
|
||||
return np.sqrt(S.sum(axis=1)**2 + C.sum(axis=1)**2) * inv_D
|
||||
|
||||
def _estimate_remaining_vibrations(self, shaper, test_damping_ratio,
|
||||
freq_bins, psd):
|
||||
vals = self._estimate_shaper(shaper, test_damping_ratio, freq_bins)
|
||||
# The input shaper can only reduce the amplitude of vibrations by
|
||||
# SHAPER_VIBRATION_REDUCTION times, so all vibrations below that
|
||||
# threshold can be igonred
|
||||
vibr_threshold = psd.max() / shaper_defs.SHAPER_VIBRATION_REDUCTION
|
||||
remaining_vibrations = self.numpy.maximum(
|
||||
vals * psd - vibr_threshold, 0).sum()
|
||||
all_vibrations = self.numpy.maximum(psd - vibr_threshold, 0).sum()
|
||||
return (remaining_vibrations / all_vibrations, vals)
|
||||
|
||||
def _get_shaper_smoothing(self, shaper, accel=5000, scv=5.):
|
||||
half_accel = accel * .5
|
||||
|
||||
A, T = shaper
|
||||
inv_D = 1. / sum(A)
|
||||
n = len(T)
|
||||
# Calculate input shaper shift
|
||||
ts = sum([A[i] * T[i] for i in range(n)]) * inv_D
|
||||
|
||||
# Calculate offset for 90 and 180 degrees turn
|
||||
offset_90 = offset_180 = 0.
|
||||
for i in range(n):
|
||||
if T[i] >= ts:
|
||||
# Calculate offset for one of the axes
|
||||
offset_90 += A[i] * (scv + half_accel * (T[i]-ts)) * (T[i]-ts)
|
||||
offset_180 += A[i] * half_accel * (T[i]-ts)**2
|
||||
offset_90 *= inv_D * math.sqrt(2.)
|
||||
offset_180 *= inv_D
|
||||
return max(offset_90, offset_180)
|
||||
|
||||
def fit_shaper(self, shaper_cfg, calibration_data, max_smoothing):
|
||||
np = self.numpy
|
||||
|
||||
test_freqs = np.arange(shaper_cfg.min_freq, MAX_SHAPER_FREQ, .2)
|
||||
|
||||
freq_bins = calibration_data.freq_bins
|
||||
psd = calibration_data.psd_sum[freq_bins <= MAX_FREQ]
|
||||
freq_bins = freq_bins[freq_bins <= MAX_FREQ]
|
||||
|
||||
best_res = None
|
||||
results = []
|
||||
for test_freq in test_freqs[::-1]:
|
||||
shaper_vibrations = 0.
|
||||
shaper_vals = np.zeros(shape=freq_bins.shape)
|
||||
shaper = shaper_cfg.init_func(
|
||||
test_freq, shaper_defs.DEFAULT_DAMPING_RATIO)
|
||||
shaper_smoothing = self._get_shaper_smoothing(shaper)
|
||||
if max_smoothing and shaper_smoothing > max_smoothing and best_res:
|
||||
return best_res
|
||||
# Exact damping ratio of the printer is unknown, pessimizing
|
||||
# remaining vibrations over possible damping values
|
||||
for dr in TEST_DAMPING_RATIOS:
|
||||
vibrations, vals = self._estimate_remaining_vibrations(
|
||||
shaper, dr, freq_bins, psd)
|
||||
shaper_vals = np.maximum(shaper_vals, vals)
|
||||
if vibrations > shaper_vibrations:
|
||||
shaper_vibrations = vibrations
|
||||
max_accel = self.find_shaper_max_accel(shaper)
|
||||
# The score trying to minimize vibrations, but also accounting
|
||||
# the growth of smoothing. The formula itself does not have any
|
||||
# special meaning, it simply shows good results on real user data
|
||||
shaper_score = shaper_smoothing * (shaper_vibrations**1.5 +
|
||||
shaper_vibrations * .2 + .01)
|
||||
results.append(
|
||||
CalibrationResult(
|
||||
name=shaper_cfg.name, freq=test_freq, vals=shaper_vals,
|
||||
vibrs=shaper_vibrations, smoothing=shaper_smoothing,
|
||||
score=shaper_score, max_accel=max_accel))
|
||||
if best_res is None or best_res.vibrs > results[-1].vibrs:
|
||||
# The current frequency is better for the shaper.
|
||||
best_res = results[-1]
|
||||
# Try to find an 'optimal' shapper configuration: the one that is not
|
||||
# much worse than the 'best' one, but gives much less smoothing
|
||||
selected = best_res
|
||||
for res in results[::-1]:
|
||||
if res.vibrs < best_res.vibrs * 1.1 and res.score < selected.score:
|
||||
selected = res
|
||||
return selected
|
||||
|
||||
def _bisect(self, func):
|
||||
left = right = 1.
|
||||
while not func(left):
|
||||
right = left
|
||||
left *= .5
|
||||
if right == left:
|
||||
while func(right):
|
||||
right *= 2.
|
||||
while right - left > 1e-8:
|
||||
middle = (left + right) * .5
|
||||
if func(middle):
|
||||
left = middle
|
||||
else:
|
||||
right = middle
|
||||
return left
|
||||
|
||||
def find_shaper_max_accel(self, shaper):
|
||||
# Just some empirically chosen value which produces good projections
|
||||
# for max_accel without much smoothing
|
||||
TARGET_SMOOTHING = 0.12
|
||||
max_accel = self._bisect(lambda test_accel: self._get_shaper_smoothing(
|
||||
shaper, test_accel) <= TARGET_SMOOTHING)
|
||||
return max_accel
|
||||
|
||||
def find_best_shaper(self, calibration_data, max_smoothing, logger=None):
|
||||
best_shaper = None
|
||||
all_shapers = []
|
||||
for shaper_cfg in shaper_defs.INPUT_SHAPERS:
|
||||
# if shaper_cfg.name not in AUTOTUNE_SHAPERS:
|
||||
if shaper_cfg.name not in self.autotune_shapers:
|
||||
continue
|
||||
shaper = self.background_process_exec(self.fit_shaper, (
|
||||
shaper_cfg, calibration_data, max_smoothing))
|
||||
if logger is not None:
|
||||
logger("Fitted shaper '%s' frequency = %.1f Hz "
|
||||
"(vibrations = %.1f%%, smoothing ~= %.3f)" % (
|
||||
shaper.name, shaper.freq, shaper.vibrs * 100.,
|
||||
shaper.smoothing))
|
||||
logger("To avoid too much smoothing with '%s', suggested "
|
||||
"max_accel <= %.0f mm/sec^2" % (
|
||||
shaper.name, round(shaper.max_accel / 100.) * 100.))
|
||||
all_shapers.append(shaper)
|
||||
if (best_shaper is None or shaper.score * 1.2 < best_shaper.score or
|
||||
(shaper.score * 1.05 < best_shaper.score and
|
||||
shaper.smoothing * 1.1 < best_shaper.smoothing)):
|
||||
# Either the shaper significantly improves the score (by 20%),
|
||||
# or it improves the score and smoothing (by 5% and 10% resp.)
|
||||
best_shaper = shaper
|
||||
return best_shaper, all_shapers
|
||||
|
||||
def save_params(self, configfile, axis, shaper_name, shaper_freq):
|
||||
if axis == 'xy':
|
||||
self.save_params(configfile, 'x', shaper_name, shaper_freq)
|
||||
self.save_params(configfile, 'y', shaper_name, shaper_freq)
|
||||
else:
|
||||
configfile.set('input_shaper', 'shaper_type_'+axis, shaper_name)
|
||||
configfile.set('input_shaper', 'shaper_freq_'+axis,
|
||||
'%.1f' % (shaper_freq,))
|
||||
|
||||
def save_calibration_data(self, output, calibration_data, shapers=None):
|
||||
try:
|
||||
with open(output, "w") as csvfile:
|
||||
csvfile.write("freq,psd_x,psd_y,psd_z,psd_xyz")
|
||||
if shapers:
|
||||
for shaper in shapers:
|
||||
csvfile.write(",%s(%.1f)" % (shaper.name, shaper.freq))
|
||||
csvfile.write("\n")
|
||||
num_freqs = calibration_data.freq_bins.shape[0]
|
||||
for i in range(num_freqs):
|
||||
if calibration_data.freq_bins[i] >= MAX_FREQ:
|
||||
break
|
||||
csvfile.write("%.1f,%.3e,%.3e,%.3e,%.3e" % (
|
||||
calibration_data.freq_bins[i],
|
||||
calibration_data.psd_x[i],
|
||||
calibration_data.psd_y[i],
|
||||
calibration_data.psd_z[i],
|
||||
calibration_data.psd_sum[i]))
|
||||
if shapers:
|
||||
for shaper in shapers:
|
||||
csvfile.write(",%.3f" % (shaper.vals[i],))
|
||||
csvfile.write("\n")
|
||||
except IOError as e:
|
||||
raise self.error({"code": "key314", "msg": "Error writing to file '%s': %s", "values":["%s", "%s"]}, output, str(e), output, str(e))
|
||||
@@ -0,0 +1,102 @@
|
||||
# Definitions of the supported input shapers
|
||||
#
|
||||
# Copyright (C) 2020-2021 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import collections, math
|
||||
|
||||
SHAPER_VIBRATION_REDUCTION=20.
|
||||
DEFAULT_DAMPING_RATIO = 0.1
|
||||
|
||||
InputShaperCfg = collections.namedtuple(
|
||||
'InputShaperCfg', ('name', 'init_func', 'min_freq'))
|
||||
|
||||
def get_none_shaper():
|
||||
return ([], [])
|
||||
|
||||
def get_zv_shaper(shaper_freq, damping_ratio):
|
||||
df = math.sqrt(1. - damping_ratio**2)
|
||||
K = math.exp(-damping_ratio * math.pi / df)
|
||||
t_d = 1. / (shaper_freq * df)
|
||||
A = [1., K]
|
||||
T = [0., .5*t_d]
|
||||
return (A, T)
|
||||
|
||||
def get_zvd_shaper(shaper_freq, damping_ratio):
|
||||
df = math.sqrt(1. - damping_ratio**2)
|
||||
K = math.exp(-damping_ratio * math.pi / df)
|
||||
t_d = 1. / (shaper_freq * df)
|
||||
A = [1., 2.*K, K**2]
|
||||
T = [0., .5*t_d, t_d]
|
||||
return (A, T)
|
||||
|
||||
def get_mzv_shaper(shaper_freq, damping_ratio):
|
||||
df = math.sqrt(1. - damping_ratio**2)
|
||||
K = math.exp(-.75 * damping_ratio * math.pi / df)
|
||||
t_d = 1. / (shaper_freq * df)
|
||||
|
||||
a1 = 1. - 1. / math.sqrt(2.)
|
||||
a2 = (math.sqrt(2.) - 1.) * K
|
||||
a3 = a1 * K * K
|
||||
|
||||
A = [a1, a2, a3]
|
||||
T = [0., .375*t_d, .75*t_d]
|
||||
return (A, T)
|
||||
|
||||
def get_ei_shaper(shaper_freq, damping_ratio):
|
||||
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
|
||||
df = math.sqrt(1. - damping_ratio**2)
|
||||
K = math.exp(-damping_ratio * math.pi / df)
|
||||
t_d = 1. / (shaper_freq * df)
|
||||
|
||||
a1 = .25 * (1. + v_tol)
|
||||
a2 = .5 * (1. - v_tol) * K
|
||||
a3 = a1 * K * K
|
||||
|
||||
A = [a1, a2, a3]
|
||||
T = [0., .5*t_d, t_d]
|
||||
return (A, T)
|
||||
|
||||
def get_2hump_ei_shaper(shaper_freq, damping_ratio):
|
||||
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
|
||||
df = math.sqrt(1. - damping_ratio**2)
|
||||
K = math.exp(-damping_ratio * math.pi / df)
|
||||
t_d = 1. / (shaper_freq * df)
|
||||
|
||||
V2 = v_tol**2
|
||||
X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.)
|
||||
a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X)
|
||||
a2 = (.5 - a1) * K
|
||||
a3 = a2 * K
|
||||
a4 = a1 * K * K * K
|
||||
|
||||
A = [a1, a2, a3, a4]
|
||||
T = [0., .5*t_d, t_d, 1.5*t_d]
|
||||
return (A, T)
|
||||
|
||||
def get_3hump_ei_shaper(shaper_freq, damping_ratio):
|
||||
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
|
||||
df = math.sqrt(1. - damping_ratio**2)
|
||||
K = math.exp(-damping_ratio * math.pi / df)
|
||||
t_d = 1. / (shaper_freq * df)
|
||||
|
||||
K2 = K*K
|
||||
a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol))
|
||||
a2 = 0.25 * (1. - v_tol) * K
|
||||
a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2
|
||||
a4 = a2 * K2
|
||||
a5 = a1 * K2 * K2
|
||||
|
||||
A = [a1, a2, a3, a4, a5]
|
||||
T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d]
|
||||
return (A, T)
|
||||
|
||||
# min_freq for each shaper is chosen to have projected max_accel ~= 1500
|
||||
INPUT_SHAPERS = [
|
||||
InputShaperCfg('zv', get_zv_shaper, min_freq=21.),
|
||||
InputShaperCfg('mzv', get_mzv_shaper, min_freq=23.),
|
||||
InputShaperCfg('zvd', get_zvd_shaper, min_freq=29.),
|
||||
InputShaperCfg('ei', get_ei_shaper, min_freq=29.),
|
||||
InputShaperCfg('2hump_ei', get_2hump_ei_shaper, min_freq=39.),
|
||||
InputShaperCfg('3hump_ei', get_3hump_ei_shaper, min_freq=48.),
|
||||
]
|
||||
@@ -0,0 +1,162 @@
|
||||
# Printer Skew Correction
|
||||
#
|
||||
# This implementation is a port of Marlin's skew correction as
|
||||
# implemented in planner.h, Copyright (C) Marlin Firmware
|
||||
#
|
||||
# https://github.com/MarlinFirmware/Marlin/tree/1.1.x/Marlin
|
||||
#
|
||||
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
import math
|
||||
|
||||
def calc_skew_factor(ac, bd, ad):
|
||||
side = math.sqrt(2*ac*ac + 2*bd*bd - 4*ad*ad) / 2.
|
||||
return math.tan(math.pi/2 - math.acos(
|
||||
(ac*ac - side*side - ad*ad) / (2*side*ad)))
|
||||
|
||||
class PrinterSkew:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.name = config.get_name()
|
||||
self.toolhead = None
|
||||
self.xy_factor = 0.
|
||||
self.xz_factor = 0.
|
||||
self.yz_factor = 0.
|
||||
self.skew_profiles = {}
|
||||
self._load_storage(config)
|
||||
self.printer.register_event_handler("klippy:connect",
|
||||
self._handle_connect)
|
||||
self.next_transform = None
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command('GET_CURRENT_SKEW', self.cmd_GET_CURRENT_SKEW,
|
||||
desc=self.cmd_GET_CURRENT_SKEW_help)
|
||||
gcode.register_command('CALC_MEASURED_SKEW',
|
||||
self.cmd_CALC_MEASURED_SKEW,
|
||||
desc=self.cmd_CALC_MEASURED_SKEW_help)
|
||||
gcode.register_command('SET_SKEW', self.cmd_SET_SKEW,
|
||||
desc=self.cmd_SET_SKEW_help)
|
||||
gcode.register_command('SKEW_PROFILE', self.cmd_SKEW_PROFILE,
|
||||
desc=self.cmd_SKEW_PROFILE_help)
|
||||
def _handle_connect(self):
|
||||
gcode_move = self.printer.lookup_object('gcode_move')
|
||||
self.next_transform = gcode_move.set_move_transform(self, force=True)
|
||||
def _load_storage(self, config):
|
||||
stored_profs = config.get_prefix_sections(self.name)
|
||||
# Remove primary skew_correction section, as it is not a stored profile
|
||||
stored_profs = [s for s in stored_profs
|
||||
if s.get_name() != self.name]
|
||||
for profile in stored_profs:
|
||||
name = profile.get_name().split(' ', 1)[1]
|
||||
self.skew_profiles[name] = {
|
||||
'xy_skew': profile.getfloat("xy_skew"),
|
||||
'xz_skew': profile.getfloat("xz_skew"),
|
||||
'yz_skew': profile.getfloat("yz_skew"),
|
||||
}
|
||||
def calc_skew(self, pos):
|
||||
skewed_x = pos[0] - pos[1] * self.xy_factor \
|
||||
- pos[2] * (self.xz_factor - (self.xy_factor * self.yz_factor))
|
||||
skewed_y = pos[1] - pos[2] * self.yz_factor
|
||||
return [skewed_x, skewed_y, pos[2], pos[3]]
|
||||
def calc_unskew(self, pos):
|
||||
skewed_x = pos[0] + pos[1] * self.xy_factor \
|
||||
+ pos[2] * self.xz_factor
|
||||
skewed_y = pos[1] + pos[2] * self.yz_factor
|
||||
return [skewed_x, skewed_y, pos[2], pos[3]]
|
||||
def get_position(self):
|
||||
return self.calc_unskew(self.next_transform.get_position())
|
||||
def move(self, newpos, speed):
|
||||
corrected_pos = self.calc_skew(newpos)
|
||||
self.next_transform.move(corrected_pos, speed)
|
||||
def _update_skew(self, xy_factor, xz_factor, yz_factor):
|
||||
self.xy_factor = xy_factor
|
||||
self.xz_factor = xz_factor
|
||||
self.yz_factor = yz_factor
|
||||
gcode_move = self.printer.lookup_object('gcode_move')
|
||||
gcode_move.reset_last_position()
|
||||
cmd_GET_CURRENT_SKEW_help = "Report current printer skew"
|
||||
def cmd_GET_CURRENT_SKEW(self, gcmd):
|
||||
out = "Current Printer Skew:"
|
||||
planes = ["XY", "XZ", "YZ"]
|
||||
factors = [self.xy_factor, self.xz_factor, self.yz_factor]
|
||||
for plane, fac in zip(planes, factors):
|
||||
out += '\n' + plane
|
||||
out += " Skew: %.6f radians, %.2f degrees" % (
|
||||
fac, math.degrees(fac))
|
||||
gcmd.respond_info(out)
|
||||
cmd_CALC_MEASURED_SKEW_help = "Calculate skew from measured print"
|
||||
def cmd_CALC_MEASURED_SKEW(self, gcmd):
|
||||
ac = gcmd.get_float("AC", above=0.)
|
||||
bd = gcmd.get_float("BD", above=0.)
|
||||
ad = gcmd.get_float("AD", above=0.)
|
||||
factor = calc_skew_factor(ac, bd, ad)
|
||||
gcmd.respond_info("Calculated Skew: %.6f radians, %.2f degrees"
|
||||
% (factor, math.degrees(factor)))
|
||||
cmd_SET_SKEW_help = "Set skew based on lengths of measured object"
|
||||
def cmd_SET_SKEW(self, gcmd):
|
||||
if gcmd.get_int("CLEAR", 0):
|
||||
self._update_skew(0., 0., 0.)
|
||||
return
|
||||
planes = ["XY", "XZ", "YZ"]
|
||||
for plane in planes:
|
||||
lengths = gcmd.get(plane, None)
|
||||
if lengths is not None:
|
||||
try:
|
||||
lengths = lengths.strip().split(",", 2)
|
||||
lengths = [float(l.strip()) for l in lengths]
|
||||
if len(lengths) != 3:
|
||||
raise Exception
|
||||
except Exception:
|
||||
raise gcmd.error(
|
||||
"""{"code": "key315", "msg": "skew_correction: improperly formatted entry for plane [%s]\n%s", "values":["%s", "%s"]}""" % (
|
||||
plane, gcmd.get_commandline(), plane, gcmd.get_commandline()))
|
||||
factor = plane.lower() + '_factor'
|
||||
setattr(self, factor, calc_skew_factor(*lengths))
|
||||
cmd_SKEW_PROFILE_help = "Profile management for skew_correction"
|
||||
def cmd_SKEW_PROFILE(self, gcmd):
|
||||
if gcmd.get('LOAD', None) is not None:
|
||||
name = gcmd.get('LOAD')
|
||||
prof = self.skew_profiles.get(name)
|
||||
if prof is None:
|
||||
gcmd.respond_info(
|
||||
"skew_correction: Load failed, unknown profile [%s]"
|
||||
% (name))
|
||||
return
|
||||
self._update_skew(prof['xy_skew'], prof['xz_skew'], prof['yz_skew'])
|
||||
elif gcmd.get('SAVE', None) is not None:
|
||||
name = gcmd.get('SAVE')
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
cfg_name = self.name + " " + name
|
||||
configfile.set(cfg_name, 'xy_skew', self.xy_factor)
|
||||
configfile.set(cfg_name, 'xz_skew', self.xz_factor)
|
||||
configfile.set(cfg_name, 'yz_skew', self.yz_factor)
|
||||
# Copy to local storage
|
||||
self.skew_profiles[name] = {
|
||||
'xy_skew': self.xy_factor,
|
||||
'xz_skew': self.xz_factor,
|
||||
'yz_skew': self.yz_factor
|
||||
}
|
||||
gcmd.respond_info(
|
||||
"Skew Correction state has been saved to profile [%s]\n"
|
||||
"for the current session. The SAVE_CONFIG command will\n"
|
||||
"update the printer config file and restart the printer."
|
||||
% (name))
|
||||
elif gcmd.get('REMOVE', None) is not None:
|
||||
name = gcmd.get('REMOVE')
|
||||
if name in self.skew_profiles:
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
configfile.remove_section('skew_correction ' + name)
|
||||
del self.skew_profiles[name]
|
||||
gcmd.respond_info(
|
||||
"Profile [%s] removed from storage for this session.\n"
|
||||
"The SAVE_CONFIG command will update the printer\n"
|
||||
"configuration and restart the printer" % (name))
|
||||
else:
|
||||
gcmd.respond_info(
|
||||
"skew_correction: No profile named [%s] to remove"
|
||||
% (name))
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return PrinterSkew(config)
|
||||
@@ -0,0 +1,154 @@
|
||||
# SmartEffector support
|
||||
#
|
||||
# Copyright (C) 2021 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
import logging
|
||||
from . import probe
|
||||
|
||||
# SmartEffector communication protocol implemented here originates from
|
||||
# https://github.com/Duet3D/SmartEffectorFirmware
|
||||
BITS_PER_SECOND = 1000.
|
||||
|
||||
class ControlPinHelper:
|
||||
def __init__(self, pin_params):
|
||||
self._mcu = pin_params['chip']
|
||||
self._pin = pin_params['pin']
|
||||
self._start_value = self._invert = pin_params['invert']
|
||||
self._oid = None
|
||||
self._set_cmd = None
|
||||
self._mcu.register_config_callback(self._build_config)
|
||||
def _build_config(self):
|
||||
self._mcu.request_move_queue_slot()
|
||||
self._oid = self._mcu.create_oid()
|
||||
self._mcu.add_config_cmd(
|
||||
"config_digital_out oid=%d pin=%s value=%d default_value=%d"
|
||||
" max_duration=%d" % (self._oid, self._pin, self._start_value,
|
||||
self._start_value, 0))
|
||||
cmd_queue = self._mcu.alloc_command_queue()
|
||||
self._set_cmd = self._mcu.lookup_command(
|
||||
"queue_digital_out oid=%c clock=%u on_ticks=%u", cq=cmd_queue)
|
||||
def write_bits(self, start_time, bit_stream):
|
||||
bit_step = 1. / BITS_PER_SECOND
|
||||
last_value = self._start_value
|
||||
bit_time = start_time
|
||||
for b in bit_stream:
|
||||
value = (not not b) ^ self._invert
|
||||
if value != last_value:
|
||||
clock = self._mcu.print_time_to_clock(bit_time)
|
||||
self._set_cmd.send([self._oid, clock, value])
|
||||
last_value = value
|
||||
bit_time += bit_step
|
||||
# After the last bit, the signal on the control pin must go back
|
||||
# to its start value.
|
||||
if value != self._start_value:
|
||||
clock = self._mcu.print_time_to_clock(bit_time)
|
||||
self._set_cmd.send([self._oid, clock, self._start_value])
|
||||
bit_time += bit_step
|
||||
return bit_time
|
||||
|
||||
class SmartEffectorEndstopWrapper:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.probe_accel = config.getfloat('probe_accel', 0., minval=0.)
|
||||
self.recovery_time = config.getfloat('recovery_time', 0.4, minval=0.)
|
||||
self.probe_wrapper = probe.ProbeEndstopWrapper(config)
|
||||
# Wrappers
|
||||
self.get_mcu = self.probe_wrapper.get_mcu
|
||||
self.add_stepper = self.probe_wrapper.add_stepper
|
||||
self.get_steppers = self.probe_wrapper.get_steppers
|
||||
self.home_start = self.probe_wrapper.home_start
|
||||
self.home_wait = self.probe_wrapper.home_wait
|
||||
self.query_endstop = self.probe_wrapper.query_endstop
|
||||
self.multi_probe_begin = self.probe_wrapper.multi_probe_begin
|
||||
self.multi_probe_end = self.probe_wrapper.multi_probe_end
|
||||
# SmartEffector control
|
||||
control_pin = config.get('control_pin', None)
|
||||
if control_pin:
|
||||
ppins = self.printer.lookup_object('pins')
|
||||
pin_params = ppins.lookup_pin(control_pin, can_invert=True)
|
||||
self.control_pin = ControlPinHelper(pin_params)
|
||||
self.gcode.register_command("RESET_SMART_EFFECTOR",
|
||||
self.cmd_RESET_SMART_EFFECTOR,
|
||||
desc=self.cmd_RESET_SMART_EFFECTOR_help)
|
||||
else:
|
||||
self.control_pin = None
|
||||
self.gcode.register_command("SET_SMART_EFFECTOR",
|
||||
self.cmd_SET_SMART_EFFECTOR,
|
||||
desc=self.cmd_SET_SMART_EFFECTOR_help)
|
||||
def probe_prepare(self, hmove):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
self.probe_wrapper.probe_prepare(hmove)
|
||||
if self.probe_accel:
|
||||
systime = self.printer.get_reactor().monotonic()
|
||||
toolhead_info = toolhead.get_status(systime)
|
||||
self.old_max_accel = toolhead_info['max_accel']
|
||||
self.gcode.run_script_from_command(
|
||||
"M204 S%.3f" % (self.probe_accel,))
|
||||
if self.recovery_time:
|
||||
toolhead.dwell(self.recovery_time)
|
||||
def probe_finish(self, hmove):
|
||||
if self.probe_accel:
|
||||
self.gcode.run_script_from_command(
|
||||
"M204 S%.3f" % (self.old_max_accel,))
|
||||
self.probe_wrapper.probe_finish(hmove)
|
||||
def _send_command(self, buf):
|
||||
# Each byte is sent to the SmartEffector as
|
||||
# [0 0 1 0 b7 b6 b5 b4 !b4 b3 b2 b1 b0 !b0]
|
||||
bit_stream = []
|
||||
for b in buf:
|
||||
b = b & 0xFF
|
||||
bit_stream.extend([0, 0, 1, 0])
|
||||
bit_stream.extend([b & 0x80, b & 0x40, b & 0x20, b & 0x10])
|
||||
bit_stream.append((~b) & 0x10)
|
||||
bit_stream.extend([b & 0x08, b & 0x04, b & 0x02, b & 0x01])
|
||||
bit_stream.append((~b) & 0x01)
|
||||
# Wait for previous actions to finish
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.wait_moves()
|
||||
start_time = toolhead.get_last_move_time()
|
||||
# Write generated bits to the control pin
|
||||
end_time = self.control_pin.write_bits(start_time, bit_stream)
|
||||
# Dwell to make sure no subseqent actions are queued together
|
||||
# with the SmartEffector programming
|
||||
toolhead.dwell(end_time - start_time)
|
||||
toolhead.wait_moves()
|
||||
cmd_SET_SMART_EFFECTOR_help = 'Set SmartEffector parameters'
|
||||
def cmd_SET_SMART_EFFECTOR(self, gcmd):
|
||||
sensitivity = gcmd.get_int('SENSITIVITY', None, minval=0, maxval=255)
|
||||
respond_info = []
|
||||
if sensitivity is not None:
|
||||
if self.control_pin is not None:
|
||||
buf = [105, sensitivity, 255 - sensitivity]
|
||||
self._send_command(buf)
|
||||
respond_info.append("sensitivity: %d" % (sensitivity,))
|
||||
else:
|
||||
raise gcmd.error("control_pin must be set in [smart_effector] "
|
||||
"for sensitivity programming")
|
||||
self.probe_accel = gcmd.get_float('ACCEL', self.probe_accel, minval=0.)
|
||||
self.recovery_time = gcmd.get_float('RECOVERY_TIME', self.recovery_time,
|
||||
minval=0.)
|
||||
if self.probe_accel:
|
||||
respond_info.append(
|
||||
"probing accelartion: %.3f" % (self.probe_accel,))
|
||||
else:
|
||||
respond_info.append("probing acceleration control disabled")
|
||||
if self.recovery_time:
|
||||
respond_info.append(
|
||||
"probe recovery time: %.3f" % (self.recovery_time,))
|
||||
else:
|
||||
respond_info.append("probe recovery time disabled")
|
||||
gcmd.respond_info("SmartEffector:\n" + "\n".join(respond_info))
|
||||
cmd_RESET_SMART_EFFECTOR_help = 'Reset SmartEffector settings (sensitivity)'
|
||||
def cmd_RESET_SMART_EFFECTOR(self, gcmd):
|
||||
buf = [131, 131]
|
||||
self._send_command(buf)
|
||||
gcmd.respond_info('SmartEffector sensitivity was reset')
|
||||
|
||||
def load_config(config):
|
||||
smart_effector = SmartEffectorEndstopWrapper(config)
|
||||
config.get_printer().add_object('probe',
|
||||
probe.PrinterProbe(config, smart_effector))
|
||||
return smart_effector
|
||||
Executable
+166
@@ -0,0 +1,166 @@
|
||||
import logging, math
|
||||
SOFT_HOMING_X_ERR_CODE = {'code':'key403', 'msg':'Homing failed due to printer shutdown,X sotf homing err', 'values':[]}
|
||||
SOFT_HOMING_Y_ERR_CODE = {'code':'key404', 'msg':'Homing failed due to printer shutdown,Y sotf homing err', 'values':[]}
|
||||
class SoftHomingInit:
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.printer = config.get_printer()
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command('SOFTX_G28', self.cmd_SOFT_G28_X)
|
||||
gcode.register_command('SOFTY_G28', self.cmd_SOFT_G28_Y)
|
||||
gcode.register_command('SOFT_CHECK_ERROR', self.cmd_SOFTX_G28_CHECK_ERROR)
|
||||
self.zeroHomeLen = config.getfloat('zeroHomeLen', default=20, minval=5, maxval=50)
|
||||
self.waitingTime_ms = config.getint('waitingTime_ms', default=2000, minval=1000, maxval=5000)
|
||||
self.diff_step = config.getint('diff_step', default=2, minval=1, maxval=10)
|
||||
self.home_cnt_max = config.getint('home_cnt_max', default=15, minval=5, maxval=30)
|
||||
self.home_mode = config.getint('home_mode', default=0, minval=0, maxval=1)
|
||||
self.check_home_falg = 0
|
||||
|
||||
|
||||
def ck_and_raise_error(self, err_code, vals=[]):
|
||||
err_code['values'] = vals
|
||||
# self.print_msg('RAISE_ERROR', str(err_code), True)
|
||||
err_code['msg'] = 'Shutdown due to ' + err_code['msg']
|
||||
# self.printer.invoke_shutdown(str(err_code))
|
||||
# while True:
|
||||
# self.delay_s(1.)
|
||||
# self.print_msg('RAISE_ERROR', str(err_code), True)
|
||||
# raise self.printer.command_error(str(err_code))
|
||||
self.printer.command_error(str(err_code))
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
gcode.respond_info(str(err_code))
|
||||
pass
|
||||
def cmd_SOFTX_G28_CHECK_ERROR(self, gcmd):
|
||||
self.check_home_falg = gcmd.get_int('FLAG', 0)
|
||||
gcmd.respond_info('self.check_home_falg:%d\n' %(self.check_home_falg))
|
||||
pass
|
||||
def cmd_SOFT_G28_X(self, gcmd):
|
||||
toolhead = self.printer.lookup_object('toolhead', None)
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
virtual_sdcard = self.printer.lookup_object('virtual_sdcard')
|
||||
kin = toolhead.get_kinematics()
|
||||
steppers = kin.get_steppers()
|
||||
gcode.run_script_from_command('G28 X')
|
||||
mcu_pos = " ".join(["%s:%d" % (s.get_name(), s.get_mcu_position())
|
||||
for s in steppers])
|
||||
step_sum = []
|
||||
break_flag = False
|
||||
pre_step = 0
|
||||
i =0
|
||||
pos = steppers[0].get_mcu_position()
|
||||
step_sum.append(pos)
|
||||
pre_step = pos
|
||||
gcmd.respond_info('step:%d\n' %(pos))
|
||||
gcmd.respond_info('steppers[0].get_mcu_position():%d\n' %(steppers[0].get_mcu_position()))
|
||||
|
||||
status_print = virtual_sdcard.get_status(1)
|
||||
p_active = status_print['is_active']
|
||||
p_pos = status_print['file_position']
|
||||
gcmd.respond_info('p_active:%d p_pos:%d\n' %(p_active, p_pos))
|
||||
if p_active > 0 and p_pos > 10:
|
||||
check_flag = True
|
||||
else :
|
||||
check_flag = False
|
||||
if self.check_home_falg == 1:
|
||||
check_flag = False
|
||||
if self.check_home_falg == 2:
|
||||
check_flag = True
|
||||
gcmd.respond_info('check_flag = %d' %(check_flag))
|
||||
while(1):
|
||||
i = i + 1
|
||||
gcode.run_script_from_command('G91')
|
||||
gcode.run_script_from_command('G0 X%f' %(self.zeroHomeLen))
|
||||
gcode.run_script_from_command('G90')
|
||||
gcode.run_script_from_command('G4 P%d'%(self.waitingTime_ms))
|
||||
gcode.run_script_from_command('G28 X')
|
||||
pos = steppers[0].get_mcu_position()
|
||||
gcmd.respond_info('step:%d\n' %(pos))
|
||||
|
||||
|
||||
if self.home_mode == 0 : #和上一次回零对比
|
||||
if ((pre_step - pos) < self.diff_step) & ((pre_step - pos) > -self.diff_step):
|
||||
break_flag = True
|
||||
break
|
||||
else: #和之前回零对比
|
||||
for temp in step_sum :
|
||||
if ((temp - pos) < self.diff_step) & ((temp - pos) > -self.diff_step):
|
||||
break_flag = True
|
||||
break
|
||||
step_sum.append(pos)
|
||||
pre_step = pos
|
||||
if(break_flag == True):
|
||||
break
|
||||
if(i >= self.home_cnt_max):
|
||||
if check_flag == True :
|
||||
self.ck_and_raise_error(SOFT_HOMING_X_ERR_CODE)
|
||||
break
|
||||
gcmd.respond_info("mcu: %s\n \n"
|
||||
% (mcu_pos))
|
||||
|
||||
|
||||
|
||||
def cmd_SOFT_G28_Y(self, gcmd):
|
||||
toolhead = self.printer.lookup_object('toolhead', None)
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
virtual_sdcard = self.printer.lookup_object('virtual_sdcard')
|
||||
kin = toolhead.get_kinematics()
|
||||
steppers = kin.get_steppers()
|
||||
gcode.run_script_from_command('G28 Y')
|
||||
mcu_pos = " ".join(["%s:%d" % (s.get_name(), s.get_mcu_position())
|
||||
for s in steppers])
|
||||
step_sum = []
|
||||
pre_step = 0
|
||||
break_flag = False
|
||||
i =0
|
||||
pos = steppers[1].get_mcu_position()
|
||||
step_sum.append(pos)
|
||||
pre_step = pos
|
||||
gcmd.respond_info('%d\n' %(pos))
|
||||
|
||||
status_print = virtual_sdcard.get_status(1)
|
||||
p_active = status_print['is_active']
|
||||
p_pos = status_print['file_position']
|
||||
gcmd.respond_info('p_active:%d p_pos:%d\n' %(p_active, p_pos))
|
||||
if p_active > 0 and p_pos > 10:
|
||||
check_flag = True
|
||||
else :
|
||||
check_flag = False
|
||||
if self.check_home_falg == 1:
|
||||
check_flag = False
|
||||
if self.check_home_falg == 2:
|
||||
check_flag = True
|
||||
gcmd.respond_info('check_flag = %d' %(check_flag))
|
||||
while(1):
|
||||
i = i + 1
|
||||
gcode.run_script_from_command('G91')
|
||||
gcode.run_script_from_command('G0 Y%f' %(self.zeroHomeLen))
|
||||
gcode.run_script_from_command('G90')
|
||||
gcode.run_script_from_command('G4 P%d'%(self.waitingTime_ms))
|
||||
gcode.run_script_from_command('G28 Y')
|
||||
pos = steppers[1].get_mcu_position()
|
||||
gcmd.respond_info('%d\n' %(pos))
|
||||
if self.home_mode == 0 :
|
||||
if ((pre_step - pos) < self.diff_step) & ((pre_step - pos) > -self.diff_step):
|
||||
break_flag = True
|
||||
break
|
||||
else:
|
||||
for temp in step_sum :
|
||||
if ((temp - pos) < self.diff_step) & ((temp - pos) > -self.diff_step):
|
||||
break_flag = True
|
||||
break
|
||||
step_sum.append(pos)
|
||||
pre_step = pos
|
||||
if(break_flag == True):
|
||||
break
|
||||
if(i >= self.home_cnt_max):
|
||||
if check_flag == True:
|
||||
self.ck_and_raise_error(SOFT_HOMING_Y_ERR_CODE)
|
||||
break
|
||||
gcmd.respond_info("mcu: %s\n \n"
|
||||
% (mcu_pos))
|
||||
|
||||
|
||||
def load_config(config):
|
||||
|
||||
return SoftHomingInit(config)
|
||||
@@ -0,0 +1,355 @@
|
||||
# Support for common SPI based thermocouple and RTD temperature sensors
|
||||
#
|
||||
# Copyright (C) 2018 Petri Honkala <cruwaller@gmail.com>
|
||||
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import math, logging
|
||||
from . import bus
|
||||
|
||||
|
||||
######################################################################
|
||||
# SensorBase
|
||||
######################################################################
|
||||
|
||||
REPORT_TIME = 0.300
|
||||
MAX_INVALID_COUNT = 3
|
||||
|
||||
class SensorBase:
|
||||
def __init__(self, config, chip_type, config_cmd=None, spi_mode=1):
|
||||
self.printer = config.get_printer()
|
||||
self.chip_type = chip_type
|
||||
self._callback = None
|
||||
self.min_sample_value = self.max_sample_value = 0
|
||||
self._report_clock = 0
|
||||
self.spi = bus.MCU_SPI_from_config(
|
||||
config, spi_mode, pin_option="sensor_pin", default_speed=4000000)
|
||||
if config_cmd is not None:
|
||||
self.spi.spi_send(config_cmd)
|
||||
self.mcu = mcu = self.spi.get_mcu()
|
||||
# Reader chip configuration
|
||||
self.oid = oid = mcu.create_oid()
|
||||
mcu.register_response(self._handle_spi_response,
|
||||
"thermocouple_result", oid)
|
||||
mcu.register_config_callback(self._build_config)
|
||||
def setup_minmax(self, min_temp, max_temp):
|
||||
adc_range = [self.calc_adc(min_temp), self.calc_adc(max_temp)]
|
||||
self.min_sample_value = min(adc_range)
|
||||
self.max_sample_value = max(adc_range)
|
||||
def setup_callback(self, cb):
|
||||
self._callback = cb
|
||||
def get_report_time_delta(self):
|
||||
return REPORT_TIME
|
||||
def _build_config(self):
|
||||
self.mcu.add_config_cmd(
|
||||
"config_thermocouple oid=%u spi_oid=%u thermocouple_type=%s" % (
|
||||
self.oid, self.spi.get_oid(), self.chip_type))
|
||||
clock = self.mcu.get_query_slot(self.oid)
|
||||
self._report_clock = self.mcu.seconds_to_clock(REPORT_TIME)
|
||||
self.mcu.add_config_cmd(
|
||||
"query_thermocouple oid=%u clock=%u rest_ticks=%u"
|
||||
" min_value=%u max_value=%u max_invalid_count=%u" % (
|
||||
self.oid, clock, self._report_clock,
|
||||
self.min_sample_value, self.max_sample_value,
|
||||
MAX_INVALID_COUNT), is_init=True)
|
||||
def _handle_spi_response(self, params):
|
||||
if params['fault']:
|
||||
self.handle_fault(params['value'], params['fault'])
|
||||
return
|
||||
temp = self.calc_temp(params['value'])
|
||||
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 report_fault(self, msg):
|
||||
logging.warn(msg)
|
||||
|
||||
|
||||
######################################################################
|
||||
# MAX31856 thermocouple
|
||||
######################################################################
|
||||
|
||||
MAX31856_CR0_REG = 0x00
|
||||
MAX31856_CR0_AUTOCONVERT = 0x80
|
||||
MAX31856_CR0_1SHOT = 0x40
|
||||
MAX31856_CR0_OCFAULT1 = 0x20
|
||||
MAX31856_CR0_OCFAULT0 = 0x10
|
||||
MAX31856_CR0_CJ = 0x08
|
||||
MAX31856_CR0_FAULT = 0x04
|
||||
MAX31856_CR0_FAULTCLR = 0x02
|
||||
MAX31856_CR0_FILT50HZ = 0x01
|
||||
MAX31856_CR0_FILT60HZ = 0x00
|
||||
|
||||
MAX31856_CR1_REG = 0x01
|
||||
MAX31856_CR1_AVGSEL1 = 0x00
|
||||
MAX31856_CR1_AVGSEL2 = 0x10
|
||||
MAX31856_CR1_AVGSEL4 = 0x20
|
||||
MAX31856_CR1_AVGSEL8 = 0x30
|
||||
MAX31856_CR1_AVGSEL16 = 0x70
|
||||
|
||||
MAX31856_MASK_REG = 0x02
|
||||
MAX31856_MASK_COLD_JUNCTION_HIGH_FAULT = 0x20
|
||||
MAX31856_MASK_COLD_JUNCTION_LOW_FAULT = 0x10
|
||||
MAX31856_MASK_THERMOCOUPLE_HIGH_FAULT = 0x08
|
||||
MAX31856_MASK_THERMOCOUPLE_LOW_FAULT = 0x04
|
||||
MAX31856_MASK_VOLTAGE_UNDER_OVER_FAULT = 0x02
|
||||
MAX31856_MASK_THERMOCOUPLE_OPEN_FAULT = 0x01
|
||||
|
||||
MAX31856_CJHF_REG = 0x03
|
||||
MAX31856_CJLF_REG = 0x04
|
||||
MAX31856_LTHFTH_REG = 0x05
|
||||
MAX31856_LTHFTL_REG = 0x06
|
||||
MAX31856_LTLFTH_REG = 0x07
|
||||
MAX31856_LTLFTL_REG = 0x08
|
||||
MAX31856_CJTO_REG = 0x09
|
||||
MAX31856_CJTH_REG = 0x0A
|
||||
MAX31856_CJTL_REG = 0x0B
|
||||
MAX31856_LTCBH_REG = 0x0C
|
||||
MAX31856_LTCBM_REG = 0x0D
|
||||
MAX31856_LTCBL_REG = 0x0E
|
||||
|
||||
MAX31856_SR_REG = 0x0F
|
||||
MAX31856_FAULT_CJRANGE = 0x80 # Cold Junction out of range
|
||||
MAX31856_FAULT_TCRANGE = 0x40 # Thermocouple out of range
|
||||
MAX31856_FAULT_CJHIGH = 0x20 # Cold Junction High
|
||||
MAX31856_FAULT_CJLOW = 0x10 # Cold Junction Low
|
||||
MAX31856_FAULT_TCHIGH = 0x08 # Thermocouple Low
|
||||
MAX31856_FAULT_TCLOW = 0x04 # Thermocouple Low
|
||||
MAX31856_FAULT_OVUV = 0x02 # Under Over Voltage
|
||||
MAX31856_FAULT_OPEN = 0x01
|
||||
|
||||
MAX31856_SCALE = 5
|
||||
MAX31856_MULT = 0.0078125
|
||||
|
||||
class MAX31856(SensorBase):
|
||||
def __init__(self, config):
|
||||
SensorBase.__init__(self, config, "MAX31856",
|
||||
self.build_spi_init(config))
|
||||
def handle_fault(self, adc, fault):
|
||||
if fault & MAX31856_FAULT_CJRANGE:
|
||||
self.report_fault("Max31856: Cold Junction Range Fault")
|
||||
if fault & MAX31856_FAULT_TCRANGE:
|
||||
self.report_fault("Max31856: Thermocouple Range Fault")
|
||||
if fault & MAX31856_FAULT_CJHIGH:
|
||||
self.report_fault("Max31856: Cold Junction High Fault")
|
||||
if fault & MAX31856_FAULT_CJLOW:
|
||||
self.report_fault("Max31856: Cold Junction Low Fault")
|
||||
if fault & MAX31856_FAULT_TCHIGH:
|
||||
self.report_fault("Max31856: Thermocouple High Fault")
|
||||
if fault & MAX31856_FAULT_TCLOW:
|
||||
self.report_fault("Max31856: Thermocouple Low Fault")
|
||||
if fault & MAX31856_FAULT_OVUV:
|
||||
self.report_fault("Max31856: Over/Under Voltage Fault")
|
||||
if fault & MAX31856_FAULT_OPEN:
|
||||
self.report_fault("Max31856: Thermocouple Open Fault")
|
||||
def calc_temp(self, adc):
|
||||
adc = adc >> MAX31856_SCALE
|
||||
# Fix sign bit:
|
||||
if adc & 0x40000:
|
||||
adc = ((adc & 0x3FFFF) + 1) * -1
|
||||
temp = MAX31856_MULT * adc
|
||||
return temp
|
||||
def calc_adc(self, temp):
|
||||
adc = int( ( temp / MAX31856_MULT ) + 0.5 ) # convert to ADC value
|
||||
adc = max(0, min(0x3FFFF, adc)) << MAX31856_SCALE
|
||||
return adc
|
||||
def build_spi_init(self, config):
|
||||
cmds = []
|
||||
value = MAX31856_CR0_AUTOCONVERT
|
||||
if config.getboolean('tc_use_50Hz_filter', False):
|
||||
value |= MAX31856_CR0_FILT50HZ
|
||||
cmds.append(0x80 + MAX31856_CR0_REG)
|
||||
cmds.append(value)
|
||||
|
||||
types = {
|
||||
"B" : 0b0000,
|
||||
"E" : 0b0001,
|
||||
"J" : 0b0010,
|
||||
"K" : 0b0011,
|
||||
"N" : 0b0100,
|
||||
"R" : 0b0101,
|
||||
"S" : 0b0110,
|
||||
"T" : 0b0111,
|
||||
}
|
||||
value = config.getchoice('tc_type', types, default="K")
|
||||
averages = {
|
||||
1 : MAX31856_CR1_AVGSEL1,
|
||||
2 : MAX31856_CR1_AVGSEL2,
|
||||
4 : MAX31856_CR1_AVGSEL4,
|
||||
8 : MAX31856_CR1_AVGSEL8,
|
||||
16 : MAX31856_CR1_AVGSEL16
|
||||
}
|
||||
value |= config.getchoice('tc_averaging_count', averages, 1)
|
||||
cmds.append(value)
|
||||
|
||||
value = (MAX31856_MASK_VOLTAGE_UNDER_OVER_FAULT |
|
||||
MAX31856_MASK_THERMOCOUPLE_OPEN_FAULT)
|
||||
cmds.append(value)
|
||||
return cmds
|
||||
|
||||
|
||||
######################################################################
|
||||
# MAX31855 thermocouple
|
||||
######################################################################
|
||||
|
||||
MAX31855_SCALE = 18
|
||||
MAX31855_MULT = 0.25
|
||||
|
||||
class MAX31855(SensorBase):
|
||||
def __init__(self, config):
|
||||
SensorBase.__init__(self, config, "MAX31855", spi_mode=0)
|
||||
def handle_fault(self, adc, fault):
|
||||
if fault & 0x1:
|
||||
self.report_fault("MAX31855 : Open Circuit")
|
||||
if fault & 0x2:
|
||||
self.report_fault("MAX31855 : Short to GND")
|
||||
if fault & 0x4:
|
||||
self.report_fault("MAX31855 : Short to Vcc")
|
||||
def calc_temp(self, adc):
|
||||
adc = adc >> MAX31855_SCALE
|
||||
# Fix sign bit:
|
||||
if adc & 0x2000:
|
||||
adc = ((adc & 0x1FFF) + 1) * -1
|
||||
temp = MAX31855_MULT * adc
|
||||
return temp
|
||||
def calc_adc(self, temp):
|
||||
adc = int( ( temp / MAX31855_MULT ) + 0.5 ) # convert to ADC value
|
||||
adc = max(0, min(0x1FFF, adc)) << MAX31855_SCALE
|
||||
return adc
|
||||
|
||||
|
||||
######################################################################
|
||||
# MAX6675 thermocouple
|
||||
######################################################################
|
||||
|
||||
MAX6675_SCALE = 3
|
||||
MAX6675_MULT = 0.25
|
||||
|
||||
class MAX6675(SensorBase):
|
||||
def __init__(self, config):
|
||||
SensorBase.__init__(self, config, "MAX6675", spi_mode=0)
|
||||
def handle_fault(self, adc, fault):
|
||||
if fault & 0x02:
|
||||
self.report_fault("Max6675 : Device ID error")
|
||||
if fault & 0x04:
|
||||
self.report_fault("Max6675 : Thermocouple Open Fault")
|
||||
def calc_temp(self, adc):
|
||||
adc = adc >> MAX6675_SCALE
|
||||
# Fix sign bit:
|
||||
if adc & 0x2000:
|
||||
adc = ((adc & 0x1FFF) + 1) * -1
|
||||
temp = MAX6675_MULT * adc
|
||||
return temp
|
||||
def calc_adc(self, temp):
|
||||
adc = int( ( temp / MAX6675_MULT ) + 0.5 ) # convert to ADC value
|
||||
adc = max(0, min(0x1FFF, adc)) << MAX6675_SCALE
|
||||
return adc
|
||||
|
||||
|
||||
######################################################################
|
||||
# MAX31865 (RTD sensor)
|
||||
######################################################################
|
||||
|
||||
MAX31865_CONFIG_REG = 0x00
|
||||
MAX31865_RTDMSB_REG = 0x01
|
||||
MAX31865_RTDLSB_REG = 0x02
|
||||
MAX31865_HFAULTMSB_REG = 0x03
|
||||
MAX31865_HFAULTLSB_REG = 0x04
|
||||
MAX31865_LFAULTMSB_REG = 0x05
|
||||
MAX31865_LFAULTLSB_REG = 0x06
|
||||
MAX31865_FAULTSTAT_REG = 0x07
|
||||
|
||||
MAX31865_CONFIG_BIAS = 0x80
|
||||
MAX31865_CONFIG_MODEAUTO = 0x40
|
||||
MAX31865_CONFIG_1SHOT = 0x20
|
||||
MAX31865_CONFIG_3WIRE = 0x10
|
||||
MAX31865_CONFIG_FAULTCLEAR = 0x02
|
||||
MAX31865_CONFIG_FILT50HZ = 0x01
|
||||
|
||||
MAX31865_FAULT_HIGHTHRESH = 0x80
|
||||
MAX31865_FAULT_LOWTHRESH = 0x40
|
||||
MAX31865_FAULT_REFINLOW = 0x20
|
||||
MAX31865_FAULT_REFINHIGH = 0x10
|
||||
MAX31865_FAULT_RTDINLOW = 0x08
|
||||
MAX31865_FAULT_OVUV = 0x04
|
||||
|
||||
MAX31865_ADC_MAX = 1<<15
|
||||
|
||||
# Callendar-Van Dusen constants for platinum resistance thermometers (RTD)
|
||||
CVD_A = 3.9083e-3
|
||||
CVD_B = -5.775e-7
|
||||
|
||||
class MAX31865(SensorBase):
|
||||
def __init__(self, config):
|
||||
rtd_nominal_r = config.getfloat('rtd_nominal_r', 100., above=0.)
|
||||
rtd_reference_r = config.getfloat('rtd_reference_r', 430., above=0.)
|
||||
adc_to_resist = rtd_reference_r / float(MAX31865_ADC_MAX)
|
||||
self.adc_to_resist_div_nominal = adc_to_resist / rtd_nominal_r
|
||||
self.config_reg = self.build_spi_init(config)
|
||||
SensorBase.__init__(self, config, "MAX31865", self.config_reg)
|
||||
def handle_fault(self, adc, fault):
|
||||
if fault & 0x80:
|
||||
self.report_fault("Max31865 RTD input is disconnected")
|
||||
if fault & 0x40:
|
||||
self.report_fault("Max31865 RTD input is shorted")
|
||||
if fault & 0x20:
|
||||
self.report_fault(
|
||||
"Max31865 VREF- is greater than 0.85 * VBIAS, FORCE- open")
|
||||
if fault & 0x10:
|
||||
self.report_fault(
|
||||
"Max31865 VREF- is less than 0.85 * VBIAS, FORCE- open")
|
||||
if fault & 0x08:
|
||||
self.report_fault(
|
||||
"Max31865 VRTD- is less than 0.85 * VBIAS, FORCE- open")
|
||||
if fault & 0x04:
|
||||
self.report_fault("Max31865 Overvoltage or undervoltage fault")
|
||||
if not fault & 0xfc:
|
||||
self.report_fault("Max31865 Unspecified error")
|
||||
# Attempt to clear the fault
|
||||
self.spi.spi_send(self.config_reg)
|
||||
def calc_temp(self, adc):
|
||||
adc = adc >> 1 # remove fault bit
|
||||
R_div_nominal = adc * self.adc_to_resist_div_nominal
|
||||
# Resistance (relative to rtd_nominal_r) is calculated using:
|
||||
# R_div_nominal = 1. + CVD_A * temp + CVD_B * temp**2
|
||||
# Solve for temp using quadratic equation:
|
||||
# temp = (-b +- sqrt(b**2 - 4ac)) / 2a
|
||||
discriminant = math.sqrt(CVD_A**2 - 4. * CVD_B * (1. - R_div_nominal))
|
||||
temp = (-CVD_A + discriminant) / (2. * CVD_B)
|
||||
return temp
|
||||
def calc_adc(self, temp):
|
||||
# Calculate relative resistance via Callendar-Van Dusen formula:
|
||||
# resistance = rtd_nominal_r * (1 + CVD_A * temp + CVD_B * temp**2)
|
||||
R_div_nominal = 1. + CVD_A * temp + CVD_B * temp * temp
|
||||
adc = int(R_div_nominal / self.adc_to_resist_div_nominal + 0.5)
|
||||
adc = max(0, min(MAX31865_ADC_MAX, adc))
|
||||
adc = adc << 1 # Add fault bit
|
||||
return adc
|
||||
def build_spi_init(self, config):
|
||||
value = (MAX31865_CONFIG_BIAS |
|
||||
MAX31865_CONFIG_MODEAUTO |
|
||||
MAX31865_CONFIG_FAULTCLEAR)
|
||||
if config.getboolean('rtd_use_50Hz_filter', False):
|
||||
value |= MAX31865_CONFIG_FILT50HZ
|
||||
if config.getint('rtd_num_of_wires', 2) == 3:
|
||||
value |= MAX31865_CONFIG_3WIRE
|
||||
cmd = 0x80 + MAX31865_CONFIG_REG
|
||||
return [cmd, value]
|
||||
|
||||
|
||||
######################################################################
|
||||
# Sensor registration
|
||||
######################################################################
|
||||
|
||||
Sensors = {
|
||||
"MAX6675": MAX6675,
|
||||
"MAX31855": MAX31855,
|
||||
"MAX31856": MAX31856,
|
||||
"MAX31865": MAX31865,
|
||||
}
|
||||
|
||||
def load_config(config):
|
||||
# Register sensors
|
||||
pheaters = config.get_printer().load_object(config, "heaters")
|
||||
for name, klass in Sensors.items():
|
||||
pheaters.add_sensor_factory(name, klass)
|
||||
@@ -0,0 +1,17 @@
|
||||
# Set the state of a list of digital output pins
|
||||
#
|
||||
# Copyright (C) 2017-2018 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
class PrinterStaticDigitalOut:
|
||||
def __init__(self, config):
|
||||
printer = config.get_printer()
|
||||
ppins = printer.lookup_object('pins')
|
||||
pin_list = config.getlist('pins')
|
||||
for pin_desc in pin_list:
|
||||
mcu_pin = ppins.setup_pin('digital_out', pin_desc)
|
||||
mcu_pin.setup_start_value(1, 1, True)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterStaticDigitalOut(config)
|
||||
@@ -0,0 +1,74 @@
|
||||
# Support for logging periodic statistics
|
||||
#
|
||||
# Copyright (C) 2018-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import os, time, logging
|
||||
|
||||
class PrinterSysStats:
|
||||
def __init__(self, config):
|
||||
printer = config.get_printer()
|
||||
self.last_process_time = self.total_process_time = 0.
|
||||
self.last_load_avg = 0.
|
||||
self.last_mem_avail = 0
|
||||
self.mem_file = None
|
||||
try:
|
||||
self.mem_file = open("/proc/meminfo", "r")
|
||||
except:
|
||||
pass
|
||||
printer.register_event_handler("klippy:disconnect", self._disconnect)
|
||||
def _disconnect(self):
|
||||
if self.mem_file is not None:
|
||||
self.mem_file.close()
|
||||
self.mem_file = None
|
||||
def stats(self, eventtime):
|
||||
# Get core usage stats
|
||||
ptime = time.process_time()
|
||||
pdiff = ptime - self.last_process_time
|
||||
self.last_process_time = ptime
|
||||
if pdiff > 0.:
|
||||
self.total_process_time += pdiff
|
||||
self.last_load_avg = os.getloadavg()[0]
|
||||
msg = "sysload=%.2f cputime=%.3f" % (self.last_load_avg,
|
||||
self.total_process_time)
|
||||
# Get available system memory
|
||||
if self.mem_file is not None:
|
||||
try:
|
||||
self.mem_file.seek(0)
|
||||
data = self.mem_file.read()
|
||||
for line in data.split('\n'):
|
||||
if line.startswith("MemAvailable:"):
|
||||
self.last_mem_avail = int(line.split()[1])
|
||||
msg = "%s memavail=%d" % (msg, self.last_mem_avail)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
return (False, msg)
|
||||
def get_status(self, eventtime):
|
||||
return {'sysload': self.last_load_avg,
|
||||
'cputime': self.total_process_time,
|
||||
'memavail': self.last_mem_avail}
|
||||
|
||||
class PrinterStats:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
reactor = self.printer.get_reactor()
|
||||
self.stats_timer = reactor.register_timer(self.generate_stats)
|
||||
self.stats_cb = []
|
||||
self.printer.register_event_handler("klippy:ready", self.handle_ready)
|
||||
def handle_ready(self):
|
||||
self.stats_cb = [o.stats for n, o in self.printer.lookup_objects()
|
||||
if hasattr(o, 'stats')]
|
||||
if self.printer.get_start_args().get('debugoutput') is None:
|
||||
reactor = self.printer.get_reactor()
|
||||
reactor.update_timer(self.stats_timer, reactor.NOW)
|
||||
def generate_stats(self, eventtime):
|
||||
stats = [cb(eventtime) for cb in self.stats_cb]
|
||||
if max([s[0] for s in stats]):
|
||||
logging.info("Stats %.1f: %s", eventtime,
|
||||
' '.join([s[1] for s in stats]))
|
||||
return eventtime + 1.
|
||||
|
||||
def load_config(config):
|
||||
config.get_printer().add_object('system_stats', PrinterSysStats(config))
|
||||
return PrinterStats(config)
|
||||
@@ -0,0 +1,133 @@
|
||||
# Support for enable pins on stepper motor drivers
|
||||
#
|
||||
# Copyright (C) 2019-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
|
||||
DISABLE_STALL_TIME = 0.100
|
||||
|
||||
# Tracking of shared stepper enable pins
|
||||
class StepperEnablePin:
|
||||
def __init__(self, mcu_enable, enable_count):
|
||||
self.mcu_enable = mcu_enable
|
||||
self.enable_count = enable_count
|
||||
self.is_dedicated = True
|
||||
def set_enable(self, print_time):
|
||||
if not self.enable_count:
|
||||
self.mcu_enable.set_digital(print_time, 1)
|
||||
self.enable_count += 1
|
||||
def set_disable(self, print_time):
|
||||
self.enable_count -= 1
|
||||
if not self.enable_count:
|
||||
self.mcu_enable.set_digital(print_time, 0)
|
||||
|
||||
def setup_enable_pin(printer, pin):
|
||||
if pin is None:
|
||||
# No enable line (stepper always enabled)
|
||||
enable = StepperEnablePin(None, 9999)
|
||||
enable.is_dedicated = False
|
||||
return enable
|
||||
ppins = printer.lookup_object('pins')
|
||||
pin_params = ppins.lookup_pin(pin, can_invert=True,
|
||||
share_type='stepper_enable')
|
||||
enable = pin_params.get('class')
|
||||
if enable is not None:
|
||||
# Shared enable line
|
||||
enable.is_dedicated = False
|
||||
return enable
|
||||
mcu_enable = pin_params['chip'].setup_pin('digital_out', pin_params)
|
||||
mcu_enable.setup_max_duration(0.)
|
||||
enable = pin_params['class'] = StepperEnablePin(mcu_enable, 0)
|
||||
return enable
|
||||
|
||||
# Enable line tracking for each stepper motor
|
||||
class EnableTracking:
|
||||
def __init__(self, stepper, enable):
|
||||
self.stepper = stepper
|
||||
self.enable = enable
|
||||
self.callbacks = []
|
||||
self.is_enabled = False
|
||||
self.stepper.add_active_callback(self.motor_enable)
|
||||
def register_state_callback(self, callback):
|
||||
self.callbacks.append(callback)
|
||||
def motor_enable(self, print_time):
|
||||
if not self.is_enabled:
|
||||
for cb in self.callbacks:
|
||||
cb(print_time, True)
|
||||
self.enable.set_enable(print_time)
|
||||
self.is_enabled = True
|
||||
def motor_disable(self, print_time):
|
||||
if self.is_enabled:
|
||||
# Enable stepper on future stepper movement
|
||||
for cb in self.callbacks:
|
||||
cb(print_time, False)
|
||||
self.enable.set_disable(print_time)
|
||||
self.is_enabled = False
|
||||
self.stepper.add_active_callback(self.motor_enable)
|
||||
def is_motor_enabled(self):
|
||||
return self.is_enabled
|
||||
def has_dedicated_enable(self):
|
||||
return self.enable.is_dedicated
|
||||
|
||||
# Global stepper enable line tracking
|
||||
class PrinterStepperEnable:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.enable_lines = {}
|
||||
self.printer.register_event_handler("gcode:request_restart",
|
||||
self._handle_request_restart)
|
||||
# Register M18/M84 commands
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command("M18", self.cmd_M18)
|
||||
gcode.register_command("M84", self.cmd_M18)
|
||||
gcode.register_command("SET_STEPPER_ENABLE",
|
||||
self.cmd_SET_STEPPER_ENABLE,
|
||||
desc=self.cmd_SET_STEPPER_ENABLE_help)
|
||||
def register_stepper(self, config, mcu_stepper):
|
||||
name = mcu_stepper.get_name()
|
||||
enable = setup_enable_pin(self.printer, config.get('enable_pin', None))
|
||||
self.enable_lines[name] = EnableTracking(mcu_stepper, enable)
|
||||
def motor_off(self):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.dwell(DISABLE_STALL_TIME)
|
||||
print_time = toolhead.get_last_move_time()
|
||||
for el in self.enable_lines.values():
|
||||
el.motor_disable(print_time)
|
||||
self.printer.send_event("stepper_enable:motor_off", print_time)
|
||||
toolhead.dwell(DISABLE_STALL_TIME)
|
||||
def motor_debug_enable(self, stepper, enable):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.dwell(DISABLE_STALL_TIME)
|
||||
print_time = toolhead.get_last_move_time()
|
||||
el = self.enable_lines[stepper]
|
||||
if enable:
|
||||
el.motor_enable(print_time)
|
||||
logging.info("%s has been manually enabled", stepper)
|
||||
else:
|
||||
el.motor_disable(print_time)
|
||||
logging.info("%s has been manually disabled", stepper)
|
||||
toolhead.dwell(DISABLE_STALL_TIME)
|
||||
def _handle_request_restart(self, print_time):
|
||||
self.motor_off()
|
||||
def cmd_M18(self, gcmd):
|
||||
# Turn off motors
|
||||
self.motor_off()
|
||||
cmd_SET_STEPPER_ENABLE_help = "Enable/disable individual stepper by name"
|
||||
def cmd_SET_STEPPER_ENABLE(self, gcmd):
|
||||
stepper_name = gcmd.get('STEPPER', None)
|
||||
if stepper_name not in self.enable_lines:
|
||||
gcmd.respond_info('SET_STEPPER_ENABLE: Invalid stepper "%s"'
|
||||
% (stepper_name,))
|
||||
return
|
||||
stepper_enable = gcmd.get_int('ENABLE', 1)
|
||||
self.motor_debug_enable(stepper_name, stepper_enable)
|
||||
def lookup_enable(self, name):
|
||||
if name not in self.enable_lines:
|
||||
raise self.printer.config_error("Unknown stepper '%s'" % (name,))
|
||||
return self.enable_lines[name]
|
||||
def get_steppers(self):
|
||||
return list(self.enable_lines.keys())
|
||||
|
||||
def load_config(config):
|
||||
return PrinterStepperEnable(config)
|
||||
@@ -0,0 +1,185 @@
|
||||
# Support fans that are enabled when temperature exceeds a set threshold
|
||||
#
|
||||
# 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
|
||||
|
||||
KELVIN_TO_CELSIUS = -273.15
|
||||
MAX_FAN_TIME = 5.0
|
||||
AMBIENT_TEMP = 25.
|
||||
PID_PARAM_BASE = 255.
|
||||
|
||||
class TemperatureFan:
|
||||
def __init__(self, config):
|
||||
self.name = config.get_name().split()[1]
|
||||
self.printer = config.get_printer()
|
||||
self.fan = fan.Fan(config, default_shutdown_speed=1.)
|
||||
self.min_temp = config.getfloat('min_temp', minval=KELVIN_TO_CELSIUS)
|
||||
self.max_temp = config.getfloat('max_temp', above=self.min_temp)
|
||||
pheaters = self.printer.load_object(config, 'heaters')
|
||||
self.sensor = pheaters.setup_sensor(config)
|
||||
self.sensor.setup_minmax(self.min_temp, self.max_temp)
|
||||
self.sensor.setup_callback(self.temperature_callback)
|
||||
pheaters.register_sensor(config, self)
|
||||
self.speed_delay = self.sensor.get_report_time_delta()
|
||||
self.max_speed_conf = config.getfloat(
|
||||
'max_speed', 1., above=0., maxval=1.)
|
||||
self.max_speed = self.max_speed_conf
|
||||
self.min_speed_conf = config.getfloat(
|
||||
'min_speed', 0.3, minval=0., maxval=1.)
|
||||
self.min_speed = self.min_speed_conf
|
||||
self.last_temp = 0.
|
||||
self.last_temp_time = 0.
|
||||
self.target_temp_conf = config.getfloat(
|
||||
'target_temp', 40. if self.max_temp > 40. else self.max_temp,
|
||||
minval=self.min_temp, maxval=self.max_temp)
|
||||
self.target_temp = self.target_temp_conf
|
||||
algos = {'watermark': ControlBangBang, 'pid': ControlPID}
|
||||
algo = config.getchoice('control', algos)
|
||||
self.control = algo(self, config)
|
||||
self.next_speed_time = 0.
|
||||
self.last_speed_value = 0.
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_mux_command(
|
||||
"SET_TEMPERATURE_FAN_TARGET", "TEMPERATURE_FAN", self.name,
|
||||
self.cmd_SET_TEMPERATURE_FAN_TARGET,
|
||||
desc=self.cmd_SET_TEMPERATURE_FAN_TARGET_help)
|
||||
|
||||
def set_speed(self, read_time, value):
|
||||
if value <= 0.:
|
||||
value = 0.
|
||||
elif value < self.min_speed:
|
||||
value = self.min_speed
|
||||
if self.target_temp <= 0.:
|
||||
value = 0.
|
||||
if ((read_time < self.next_speed_time or not self.last_speed_value)
|
||||
and abs(value - self.last_speed_value) < 0.05):
|
||||
# No significant change in value - can suppress update
|
||||
return
|
||||
speed_time = read_time + self.speed_delay
|
||||
self.next_speed_time = speed_time + 0.75 * MAX_FAN_TIME
|
||||
self.last_speed_value = value
|
||||
self.fan.set_speed(speed_time, value)
|
||||
def temperature_callback(self, read_time, temp):
|
||||
self.last_temp = temp
|
||||
self.control.temperature_callback(read_time, temp)
|
||||
def get_temp(self, eventtime):
|
||||
return self.last_temp, self.target_temp
|
||||
def get_min_speed(self):
|
||||
return self.min_speed
|
||||
def get_max_speed(self):
|
||||
return self.max_speed
|
||||
def get_status(self, eventtime):
|
||||
status = self.fan.get_status(eventtime)
|
||||
status["temperature"] = round(self.last_temp, 2)
|
||||
status["target"] = self.target_temp
|
||||
return status
|
||||
cmd_SET_TEMPERATURE_FAN_TARGET_help = \
|
||||
"Sets a temperature fan target and fan speed limits"
|
||||
def cmd_SET_TEMPERATURE_FAN_TARGET(self, gcmd):
|
||||
temp = gcmd.get_float('TARGET', self.target_temp_conf)
|
||||
self.set_temp(temp)
|
||||
min_speed = gcmd.get_float('MIN_SPEED', self.min_speed)
|
||||
max_speed = gcmd.get_float('MAX_SPEED', self.max_speed)
|
||||
if min_speed > max_speed:
|
||||
raise self.printer.command_error(
|
||||
"Requested min speed (%.1f) is greater than max speed (%.1f)"
|
||||
% (min_speed, max_speed))
|
||||
self.set_min_speed(min_speed)
|
||||
self.set_max_speed(max_speed)
|
||||
|
||||
def set_temp(self, degrees):
|
||||
if degrees and (degrees < self.min_temp or degrees > self.max_temp):
|
||||
raise self.printer.command_error(
|
||||
"""{"code":"key339", "msg":"TemperatureFan %s Requested temperature (%.1f) out of range (%.1f:%.1f)", "values":["%s", %.1f, %.1f, %.1f]}"""
|
||||
% (self.name, degrees, self.min_temp, self.max_temp, self.name, degrees, self.min_temp, self.max_temp))
|
||||
self.target_temp = degrees
|
||||
|
||||
def set_min_speed(self, speed):
|
||||
if speed and (speed < 0. or speed > 1.):
|
||||
raise self.printer.command_error(
|
||||
"Requested min speed (%.1f) out of range (0.0 : 1.0)"
|
||||
% (speed))
|
||||
self.min_speed = speed
|
||||
|
||||
def set_max_speed(self, speed):
|
||||
if speed and (speed < 0. or speed > 1.):
|
||||
raise self.printer.command_error(
|
||||
"Requested max speed (%.1f) out of range (0.0 : 1.0)"
|
||||
% (speed))
|
||||
self.max_speed = speed
|
||||
|
||||
######################################################################
|
||||
# Bang-bang control algo
|
||||
######################################################################
|
||||
|
||||
class ControlBangBang:
|
||||
def __init__(self, temperature_fan, config):
|
||||
self.temperature_fan = temperature_fan
|
||||
self.max_delta = config.getfloat('max_delta', 2.0, above=0.)
|
||||
self.heating = False
|
||||
def temperature_callback(self, read_time, temp):
|
||||
current_temp, target_temp = self.temperature_fan.get_temp(read_time)
|
||||
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.temperature_fan.set_speed(read_time, 0.)
|
||||
else:
|
||||
self.temperature_fan.set_speed(read_time,
|
||||
self.temperature_fan.get_max_speed())
|
||||
|
||||
######################################################################
|
||||
# Proportional Integral Derivative (PID) control algo
|
||||
######################################################################
|
||||
|
||||
PID_SETTLE_DELTA = 1.
|
||||
PID_SETTLE_SLOPE = .1
|
||||
|
||||
class ControlPID:
|
||||
def __init__(self, temperature_fan, config):
|
||||
self.temperature_fan = temperature_fan
|
||||
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 = config.getfloat('pid_deriv_time', 2., above=0.)
|
||||
self.temp_integ_max = 0.
|
||||
if self.Ki:
|
||||
self.temp_integ_max = self.temperature_fan.get_max_speed() / self.Ki
|
||||
self.prev_temp = AMBIENT_TEMP
|
||||
self.prev_temp_time = 0.
|
||||
self.prev_temp_deriv = 0.
|
||||
self.prev_temp_integ = 0.
|
||||
def temperature_callback(self, read_time, temp):
|
||||
current_temp, target_temp = self.temperature_fan.get_temp(read_time)
|
||||
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
|
||||
bounded_co = max(0., min(self.temperature_fan.get_max_speed(), co))
|
||||
self.temperature_fan.set_speed(
|
||||
read_time, max(self.temperature_fan.get_min_speed(),
|
||||
self.temperature_fan.get_max_speed() - 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 load_config_prefix(config):
|
||||
return TemperatureFan(config)
|
||||
@@ -0,0 +1,80 @@
|
||||
# Support for Raspberry Pi temperature sensor
|
||||
#
|
||||
# Copyright (C) 2020 Al Crate <al3ph@users.noreply.github.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
import logging
|
||||
|
||||
HOST_REPORT_TIME = 1.0
|
||||
RPI_PROC_TEMP_FILE = "/sys/class/thermal/thermal_zone0/temp"
|
||||
|
||||
class Temperature_HOST:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.reactor = self.printer.get_reactor()
|
||||
self.name = config.get_name().split()[-1]
|
||||
self.path = config.get("sensor_path", RPI_PROC_TEMP_FILE)
|
||||
|
||||
self.temp = self.min_temp = self.max_temp = 0.0
|
||||
|
||||
self.printer.add_object("temperature_host " + self.name, self)
|
||||
if self.printer.get_start_args().get('debugoutput') is not None:
|
||||
return
|
||||
self.sample_timer = self.reactor.register_timer(
|
||||
self._sample_pi_temperature)
|
||||
try:
|
||||
self.file_handle = open(self.path, "r")
|
||||
except:
|
||||
raise config.error("Unable to open temperature file '%s'"
|
||||
% (self.path,))
|
||||
|
||||
self.printer.register_event_handler("klippy:connect",
|
||||
self.handle_connect)
|
||||
|
||||
def handle_connect(self):
|
||||
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 HOST_REPORT_TIME
|
||||
|
||||
def _sample_pi_temperature(self, eventtime):
|
||||
try:
|
||||
self.file_handle.seek(0)
|
||||
self.temp = float(self.file_handle.read())/1000.0
|
||||
except Exception:
|
||||
logging.exception("temperature_host: Error reading data")
|
||||
self.temp = 0.0
|
||||
return self.reactor.NEVER
|
||||
|
||||
if self.temp < self.min_temp:
|
||||
self.printer.invoke_shutdown(
|
||||
"HOST temperature %0.1f below minimum temperature of %0.1f."
|
||||
% (self.temp, self.min_temp,))
|
||||
if self.temp > self.max_temp:
|
||||
self.printer.invoke_shutdown(
|
||||
"HOST temperature %0.1f above maximum temperature of %0.1f."
|
||||
% (self.temp, self.max_temp,))
|
||||
|
||||
mcu = self.printer.lookup_object('mcu')
|
||||
measured_time = self.reactor.monotonic()
|
||||
self._callback(mcu.estimated_print_time(measured_time), self.temp)
|
||||
return measured_time + HOST_REPORT_TIME
|
||||
|
||||
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("temperature_host", Temperature_HOST)
|
||||
@@ -0,0 +1,173 @@
|
||||
# Support for micro-controller chip based temperature sensors
|
||||
#
|
||||
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
import mcu
|
||||
|
||||
SAMPLE_TIME = 0.001
|
||||
SAMPLE_COUNT = 8
|
||||
REPORT_TIME = 0.300
|
||||
RANGE_CHECK_COUNT = 4
|
||||
|
||||
class PrinterTemperatureMCU:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.base_temperature = self.slope = None
|
||||
self.temp1 = self.adc1 = self.temp2 = self.adc2 = None
|
||||
self.min_temp = self.max_temp = 0.
|
||||
self.debug_read_cmd = None
|
||||
# Read config
|
||||
mcu_name = config.get('sensor_mcu', 'mcu')
|
||||
self.temp1 = config.getfloat('sensor_temperature1', None)
|
||||
if self.temp1 is not None:
|
||||
self.adc1 = config.getfloat('sensor_adc1', minval=0., maxval=1.)
|
||||
self.temp2 = config.getfloat('sensor_temperature2', None)
|
||||
if self.temp2 is not None:
|
||||
self.adc2 = config.getfloat('sensor_adc2', minval=0., maxval=1.)
|
||||
# Setup ADC port
|
||||
ppins = config.get_printer().lookup_object('pins')
|
||||
self.mcu_adc = ppins.setup_pin('adc',
|
||||
'%s:ADC_TEMPERATURE' % (mcu_name,))
|
||||
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)
|
||||
# Register callbacks
|
||||
if self.printer.get_start_args().get('debugoutput') is not None:
|
||||
self.mcu_adc.setup_minmax(SAMPLE_TIME, SAMPLE_COUNT,
|
||||
range_check_count=RANGE_CHECK_COUNT)
|
||||
return
|
||||
self.printer.register_event_handler("klippy:mcu_identify",
|
||||
self._mcu_identify)
|
||||
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.base_temperature + read_value * self.slope
|
||||
self.temperature_callback(read_time + SAMPLE_COUNT * SAMPLE_TIME, temp)
|
||||
def setup_minmax(self, min_temp, max_temp):
|
||||
self.min_temp = min_temp
|
||||
self.max_temp = max_temp
|
||||
def calc_adc(self, temp):
|
||||
return (temp - self.base_temperature) / self.slope
|
||||
def calc_base(self, temp, adc):
|
||||
return temp - adc * self.slope
|
||||
def _mcu_identify(self):
|
||||
# Obtain mcu information
|
||||
mcu = self.mcu_adc.get_mcu()
|
||||
self.debug_read_cmd = mcu.lookup_query_command(
|
||||
"debug_read order=%c addr=%u", "debug_result val=%u")
|
||||
self.mcu_type = mcu.get_constants().get("MCU", "")
|
||||
# Run MCU specific configuration
|
||||
cfg_funcs = [
|
||||
('rp2040', self.config_rp2040),
|
||||
('sam3', self.config_sam3), ('sam4', self.config_sam4),
|
||||
('same70', self.config_same70), ('samd21', self.config_samd21),
|
||||
('samd51', self.config_samd51), ('same5', self.config_samd51),
|
||||
('stm32f1', self.config_stm32f1), ('stm32f2', self.config_stm32f2),
|
||||
('stm32f4', self.config_stm32f4),
|
||||
('stm32f042', self.config_stm32f0x2),
|
||||
('stm32f070', self.config_stm32f070),
|
||||
('stm32f072', self.config_stm32f0x2),
|
||||
('stm32g0', self.config_stm32g0),
|
||||
('stm32g4', self.config_stm32g0),
|
||||
('stm32l4', self.config_stm32g0),
|
||||
('stm32h723', self.config_stm32h723),
|
||||
('stm32h7', self.config_stm32h7),
|
||||
('gd32f303xe', self.config_gd32f303xe),
|
||||
('', self.config_unknown)]
|
||||
for name, func in cfg_funcs:
|
||||
if self.mcu_type.startswith(name):
|
||||
func()
|
||||
break
|
||||
logging.info("mcu_temperature '%s' nominal base=%.6f slope=%.6f",
|
||||
mcu.get_name(), self.base_temperature, self.slope)
|
||||
# Setup manual base/slope override
|
||||
if self.temp1 is not None:
|
||||
if self.temp2 is not None:
|
||||
self.slope = (self.temp2 - self.temp1) / (self.adc2 - self.adc1)
|
||||
self.base_temperature = self.calc_base(self.temp1, self.adc1)
|
||||
# Setup min/max checks
|
||||
adc_range = [self.calc_adc(t) for t in [self.min_temp, self.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)
|
||||
def config_unknown(self):
|
||||
raise self.printer.config_error("MCU temperature not supported on %s"
|
||||
% (self.mcu_type,))
|
||||
def config_rp2040(self):
|
||||
self.slope = 3.3 / -0.001721
|
||||
self.base_temperature = self.calc_base(27., 0.706 / 3.3)
|
||||
def config_sam3(self):
|
||||
self.slope = 3.3 / .002650
|
||||
self.base_temperature = self.calc_base(27., 0.8 / 3.3)
|
||||
def config_sam4(self):
|
||||
self.slope = 3.3 / .004700
|
||||
self.base_temperature = self.calc_base(27., 1.44 / 3.3)
|
||||
def config_same70(self):
|
||||
self.slope = 3.3 / .002330
|
||||
self.base_temperature = self.calc_base(25., 0.72 / 3.3)
|
||||
def config_samd21(self, addr=0x00806030):
|
||||
def get1v(val):
|
||||
if val & 0x80:
|
||||
val = 0x100 - val
|
||||
return 1. - val / 1000.
|
||||
cal1 = self.read32(addr)
|
||||
cal2 = self.read32(addr + 4)
|
||||
room_temp = ((cal1 >> 0) & 0xff) + ((cal1 >> 8) & 0xf) / 10.
|
||||
hot_temp = ((cal1 >> 12) & 0xff) + ((cal1 >> 20) & 0xf) / 10.
|
||||
room_1v = get1v((cal1 >> 24) & 0xff)
|
||||
hot_1v = get1v((cal2 >> 0) & 0xff)
|
||||
room_adc = ((cal2 >> 8) & 0xfff) * room_1v / (3.3 * 4095.)
|
||||
hot_adc = ((cal2 >> 20) & 0xfff) * hot_1v / (3.3 * 4095.)
|
||||
self.slope = (hot_temp - room_temp) / (hot_adc - room_adc)
|
||||
self.base_temperature = self.calc_base(room_temp, room_adc)
|
||||
def config_samd51(self):
|
||||
self.config_samd21(addr=0x00800100)
|
||||
def config_stm32f1(self):
|
||||
self.slope = 3.3 / -.004300
|
||||
self.base_temperature = self.calc_base(25., 1.43 / 3.3)
|
||||
def config_stm32f2(self):
|
||||
self.slope = 3.3 / .002500
|
||||
self.base_temperature = self.calc_base(25., .76 / 3.3)
|
||||
def config_stm32f4(self, addr1=0x1FFF7A2C, addr2=0x1FFF7A2E):
|
||||
cal_adc_30 = self.read16(addr1) / 4095.
|
||||
cal_adc_110 = self.read16(addr2) / 4095.
|
||||
self.slope = (110. - 30.) / (cal_adc_110 - cal_adc_30)
|
||||
self.base_temperature = self.calc_base(30., cal_adc_30)
|
||||
def config_stm32f0x2(self):
|
||||
self.config_stm32f4(addr1=0x1FFFF7B8, addr2=0x1FFFF7C2)
|
||||
def config_stm32f070(self):
|
||||
self.slope = 3.3 / -.004300
|
||||
cal_adc_30 = self.read16(0x1FFFF7B8) / 4095.
|
||||
self.base_temperature = self.calc_base(30., cal_adc_30)
|
||||
def config_stm32g0(self):
|
||||
cal_adc_30 = self.read16(0x1FFF75A8) * 3.0 / (3.3 * 4095.)
|
||||
cal_adc_130 = self.read16(0x1FFF75CA) * 3.0 / (3.3 * 4095.)
|
||||
self.slope = (130. - 30.) / (cal_adc_130 - cal_adc_30)
|
||||
self.base_temperature = self.calc_base(30., cal_adc_30)
|
||||
def config_stm32h723(self):
|
||||
cal_adc_30 = self.read16(0x1FF1E820) / 4095.
|
||||
cal_adc_130 = self.read16(0x1FF1E840) / 4095.
|
||||
self.slope = (130. - 30.) / (cal_adc_130 - cal_adc_30)
|
||||
self.base_temperature = self.calc_base(30., cal_adc_30)
|
||||
def config_stm32h7(self):
|
||||
cal_adc_30 = self.read16(0x1FF1E820) / 65535.
|
||||
cal_adc_110 = self.read16(0x1FF1E840) / 65535.
|
||||
self.slope = (110. - 30.) / (cal_adc_110 - cal_adc_30)
|
||||
self.base_temperature = self.calc_base(30., cal_adc_30)
|
||||
def config_gd32f303xe(self):
|
||||
self.slope = 3.3 / -.004100
|
||||
self.base_temperature = self.calc_base(25., 1.45 / 3.3)
|
||||
def read16(self, addr):
|
||||
params = self.debug_read_cmd.send([1, addr])
|
||||
return params['val']
|
||||
def read32(self, addr):
|
||||
params = self.debug_read_cmd.send([2, addr])
|
||||
return params['val']
|
||||
|
||||
def load_config(config):
|
||||
pheaters = config.get_printer().load_object(config, "heaters")
|
||||
pheaters.add_sensor_factory("temperature_mcu", PrinterTemperatureMCU)
|
||||
@@ -0,0 +1,42 @@
|
||||
# Support generic temperature sensors
|
||||
#
|
||||
# Copyright (C) 2019 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
KELVIN_TO_CELSIUS = -273.15
|
||||
|
||||
class PrinterSensorGeneric:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.name = config.get_name().split()[-1]
|
||||
pheaters = self.printer.load_object(config, 'heaters')
|
||||
self.sensor = pheaters.setup_sensor(config)
|
||||
self.min_temp = config.getfloat('min_temp', KELVIN_TO_CELSIUS,
|
||||
minval=KELVIN_TO_CELSIUS)
|
||||
self.max_temp = config.getfloat('max_temp', 99999999.9,
|
||||
above=self.min_temp)
|
||||
self.sensor.setup_minmax(self.min_temp, self.max_temp)
|
||||
self.sensor.setup_callback(self.temperature_callback)
|
||||
pheaters.register_sensor(config, self)
|
||||
self.last_temp = 0.
|
||||
self.measured_min = 99999999.
|
||||
self.measured_max = 0.
|
||||
def temperature_callback(self, read_time, temp):
|
||||
self.last_temp = temp
|
||||
if temp:
|
||||
self.measured_min = min(self.measured_min, temp)
|
||||
self.measured_max = max(self.measured_max, temp)
|
||||
def get_temp(self, eventtime):
|
||||
return self.last_temp, 0.
|
||||
def stats(self, eventtime):
|
||||
return False, '%s: temp=%.1f' % (self.name, self.last_temp)
|
||||
def get_status(self, eventtime):
|
||||
return {
|
||||
'temperature': round(self.last_temp, 2),
|
||||
'measured_min_temp': round(self.measured_min, 2),
|
||||
'measured_max_temp': round(self.measured_max, 2)
|
||||
}
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterSensorGeneric(config)
|
||||
@@ -0,0 +1,109 @@
|
||||
# This file loads the default temperature sensors.
|
||||
|
||||
|
||||
########################################
|
||||
# Module loading
|
||||
########################################
|
||||
|
||||
# Load "PT1000", "PT100 INA826", "AD595", "AD597", "AD8494", "AD8495",
|
||||
# "AD8496", and "AD8497" sensors
|
||||
[adc_temperature]
|
||||
|
||||
# Load "BME280" sensor
|
||||
[bme280]
|
||||
|
||||
# Load "DS18B20" sensor
|
||||
[ds18b20]
|
||||
|
||||
# Load "SI7013", "SI7020", "SI7021", "SHT21", and "HTU21D" sensors
|
||||
[htu21d]
|
||||
|
||||
# Load "LM75" sensor
|
||||
[lm75]
|
||||
|
||||
# Load "MAX6675", "MAX31855", "MAX31856", and "MAX31865" sensors
|
||||
[spi_temperature]
|
||||
|
||||
# Load "temperature_host" sensor
|
||||
[temperature_host]
|
||||
|
||||
# Load "temperature_mcu" sensor
|
||||
[temperature_mcu]
|
||||
|
||||
|
||||
########################################
|
||||
# Default thermistors
|
||||
########################################
|
||||
|
||||
# Definition from (20211101): https://download.lulzbot.com/retail_parts/Completed_Parts/100k_Semitech_GT2_Thermistor_KT-EL0059/GT-2-glass-thermistors.pdf
|
||||
[thermistor ATC Semitec 104GT-2]
|
||||
temperature1: 20
|
||||
resistance1: 126800
|
||||
temperature2: 150
|
||||
resistance2: 1360
|
||||
temperature3: 300
|
||||
resistance3: 80.65
|
||||
|
||||
# Definition from (20211112): https://atcsemitec.co.uk/wp-content/uploads/2019/01/Semitec-NT-4-Glass-NTC-Thermistor.pdf
|
||||
[thermistor ATC Semitec 104NT-4-R025H42G]
|
||||
temperature1: 25
|
||||
resistance1: 100000
|
||||
temperature2: 160
|
||||
resistance2: 1074
|
||||
temperature3: 300
|
||||
resistance3: 82.78
|
||||
|
||||
# Definition from (20211101): https://www.tdk-electronics.tdk.com/inf/50/db/ntc_09/Glass_enc_Sensors__B57560__G560__G1560.pdf
|
||||
# (B57560G104 is same definition as B57560G1104)
|
||||
[thermistor EPCOS 100K B57560G104F]
|
||||
temperature1: 25
|
||||
resistance1: 100000
|
||||
temperature2: 150
|
||||
resistance2: 1641.9
|
||||
temperature3: 250
|
||||
resistance3: 226.15
|
||||
|
||||
# Definition from (20211101): https://www.keenovo.com/NTC-Thermistor-R-T-Table.pdf
|
||||
[thermistor Generic 3950]
|
||||
temperature1: 25
|
||||
resistance1: 100000
|
||||
temperature2: 150
|
||||
resistance2: 1770
|
||||
temperature3: 250
|
||||
resistance3: 230
|
||||
|
||||
# Definition from (20211101): https://www.sliceengineering.com/products/thermistor-high-temperature and https://docs.google.com/spreadsheets/d/1904x5JK-Sup-cX5DqHiiZWaFVTK6_PQBFxgi_6yXEJw/edit#gid=0
|
||||
[thermistor SliceEngineering 450]
|
||||
temperature1: 25
|
||||
resistance1: 500000
|
||||
temperature2: 200
|
||||
resistance2: 3734
|
||||
temperature3: 400
|
||||
resistance3: 240
|
||||
|
||||
# Definition from (20211101): https://product.tdk.com/system/files/dam/doc/product/sensor/ntc/chip-ntc-thermistor/rt_sheets/ntcg104lh104jt1.csv
|
||||
[thermistor TDK NTCG104LH104JT1]
|
||||
temperature1: 25
|
||||
resistance1: 100000
|
||||
temperature2: 50
|
||||
resistance2: 31230
|
||||
temperature3: 125
|
||||
resistance3: 2066
|
||||
|
||||
# Definition from (20211101): https://sensing.honeywell.com/135-104lag-j01-thermistors
|
||||
[thermistor Honeywell 100K 135-104LAG-J01]
|
||||
temperature1: 25
|
||||
resistance1: 100000
|
||||
beta: 3974
|
||||
|
||||
# Definition inherent from name. This sensor is deprecated!
|
||||
[thermistor NTC 100K beta 3950]
|
||||
temperature1: 25
|
||||
resistance1: 100000
|
||||
beta: 3950
|
||||
|
||||
# Definition from description of Marlin "thermistor 75"
|
||||
[thermistor NTC 100K MGB18-104F39050L32]
|
||||
temperature1: 25
|
||||
resistance1: 100000
|
||||
beta: 4100
|
||||
@@ -0,0 +1,107 @@
|
||||
# Temperature measurements with thermistors
|
||||
#
|
||||
# Copyright (C) 2016-2019 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import math, logging
|
||||
from . import adc_temperature
|
||||
|
||||
KELVIN_TO_CELSIUS = -273.15
|
||||
|
||||
# Analog voltage to temperature converter for thermistors
|
||||
class Thermistor:
|
||||
def __init__(self, pullup, inline_resistor):
|
||||
self.pullup = pullup
|
||||
self.inline_resistor = inline_resistor
|
||||
self.c1 = self.c2 = self.c3 = 0.
|
||||
def setup_coefficients(self, t1, r1, t2, r2, t3, r3, name=""):
|
||||
# Calculate Steinhart-Hart coefficents from temp measurements.
|
||||
# Arrange samples as 3 linear equations and solve for c1, c2, and c3.
|
||||
inv_t1 = 1. / (t1 - KELVIN_TO_CELSIUS)
|
||||
inv_t2 = 1. / (t2 - KELVIN_TO_CELSIUS)
|
||||
inv_t3 = 1. / (t3 - KELVIN_TO_CELSIUS)
|
||||
ln_r1 = math.log(r1)
|
||||
ln_r2 = math.log(r2)
|
||||
ln_r3 = math.log(r3)
|
||||
ln3_r1, ln3_r2, ln3_r3 = ln_r1**3, ln_r2**3, ln_r3**3
|
||||
|
||||
inv_t12, inv_t13 = inv_t1 - inv_t2, inv_t1 - inv_t3
|
||||
ln_r12, ln_r13 = ln_r1 - ln_r2, ln_r1 - ln_r3
|
||||
ln3_r12, ln3_r13 = ln3_r1 - ln3_r2, ln3_r1 - ln3_r3
|
||||
|
||||
self.c3 = ((inv_t12 - inv_t13 * ln_r12 / ln_r13)
|
||||
/ (ln3_r12 - ln3_r13 * ln_r12 / ln_r13))
|
||||
if self.c3 <= 0.:
|
||||
beta = ln_r13 / inv_t13
|
||||
logging.warn("Using thermistor beta %.3f in heater %s", beta, name)
|
||||
self.setup_coefficients_beta(t1, r1, beta)
|
||||
return
|
||||
self.c2 = (inv_t12 - self.c3 * ln3_r12) / ln_r12
|
||||
self.c1 = inv_t1 - self.c2 * ln_r1 - self.c3 * ln3_r1
|
||||
def setup_coefficients_beta(self, t1, r1, beta):
|
||||
# Calculate equivalent Steinhart-Hart coefficents from beta
|
||||
inv_t1 = 1. / (t1 - KELVIN_TO_CELSIUS)
|
||||
ln_r1 = math.log(r1)
|
||||
self.c3 = 0.
|
||||
self.c2 = 1. / beta
|
||||
self.c1 = inv_t1 - self.c2 * ln_r1
|
||||
def calc_temp(self, adc):
|
||||
# Calculate temperature from adc
|
||||
adc = max(.00001, min(.99999, adc))
|
||||
r = self.pullup * adc / (1.0 - adc)
|
||||
ln_r = math.log(r - self.inline_resistor)
|
||||
inv_t = self.c1 + self.c2 * ln_r + self.c3 * ln_r**3
|
||||
return 1.0/inv_t + KELVIN_TO_CELSIUS
|
||||
def calc_adc(self, temp):
|
||||
# Calculate adc reading from a temperature
|
||||
if temp <= KELVIN_TO_CELSIUS:
|
||||
return 1.
|
||||
inv_t = 1. / (temp - KELVIN_TO_CELSIUS)
|
||||
if self.c3:
|
||||
# Solve for ln_r using Cardano's formula
|
||||
y = (self.c1 - inv_t) / (2. * self.c3)
|
||||
x = math.sqrt((self.c2 / (3. * self.c3))**3 + y**2)
|
||||
ln_r = math.pow(x - y, 1./3.) - math.pow(x + y, 1./3.)
|
||||
else:
|
||||
ln_r = (inv_t - self.c1) / self.c2
|
||||
r = math.exp(ln_r) + self.inline_resistor
|
||||
return r / (self.pullup + r)
|
||||
|
||||
# Create an ADC converter with a thermistor
|
||||
def PrinterThermistor(config, params):
|
||||
pullup = config.getfloat('pullup_resistor', 4700., above=0.)
|
||||
inline_resistor = config.getfloat('inline_resistor', 0., minval=0.)
|
||||
thermistor = Thermistor(pullup, inline_resistor)
|
||||
if 'beta' in params:
|
||||
thermistor.setup_coefficients_beta(
|
||||
params['t1'], params['r1'], params['beta'])
|
||||
else:
|
||||
thermistor.setup_coefficients(
|
||||
params['t1'], params['r1'], params['t2'], params['r2'],
|
||||
params['t3'], params['r3'], name=config.get_name())
|
||||
return adc_temperature.PrinterADCtoTemperature(config, thermistor)
|
||||
|
||||
# Custom defined thermistors from the config file
|
||||
class CustomThermistor:
|
||||
def __init__(self, config):
|
||||
self.name = " ".join(config.get_name().split()[1:])
|
||||
t1 = config.getfloat("temperature1", minval=KELVIN_TO_CELSIUS)
|
||||
r1 = config.getfloat("resistance1", minval=0.)
|
||||
beta = config.getfloat("beta", None, above=0.)
|
||||
if beta is not None:
|
||||
self.params = {'t1': t1, 'r1': r1, 'beta': beta}
|
||||
return
|
||||
t2 = config.getfloat("temperature2", minval=KELVIN_TO_CELSIUS)
|
||||
r2 = config.getfloat("resistance2", minval=0.)
|
||||
t3 = config.getfloat("temperature3", minval=KELVIN_TO_CELSIUS)
|
||||
r3 = config.getfloat("resistance3", minval=0.)
|
||||
(t1, r1), (t2, r2), (t3, r3) = sorted([(t1, r1), (t2, r2), (t3, r3)])
|
||||
self.params = {'t1': t1, 'r1': r1, 't2': t2, 'r2': r2,
|
||||
't3': t3, 'r3': r3}
|
||||
def create(self, config):
|
||||
return PrinterThermistor(config, self.params)
|
||||
|
||||
def load_config_prefix(config):
|
||||
thermistor = CustomThermistor(config)
|
||||
pheaters = config.get_printer().load_object(config, "heaters")
|
||||
pheaters.add_sensor_factory(thermistor.name, thermistor.create)
|
||||
@@ -0,0 +1,563 @@
|
||||
# Common helper code for TMC stepper drivers
|
||||
#
|
||||
# Copyright (C) 2018-2020 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging, collections
|
||||
import stepper
|
||||
|
||||
|
||||
######################################################################
|
||||
# Field helpers
|
||||
######################################################################
|
||||
|
||||
# Return the position of the first bit set in a mask
|
||||
def ffs(mask):
|
||||
return (mask & -mask).bit_length() - 1
|
||||
|
||||
class FieldHelper:
|
||||
def __init__(self, all_fields, signed_fields=[], field_formatters={},
|
||||
registers=None):
|
||||
self.all_fields = all_fields
|
||||
self.signed_fields = {sf: 1 for sf in signed_fields}
|
||||
self.field_formatters = field_formatters
|
||||
self.registers = registers
|
||||
if self.registers is None:
|
||||
self.registers = collections.OrderedDict()
|
||||
self.field_to_register = { f: r for r, fields in self.all_fields.items()
|
||||
for f in fields }
|
||||
def lookup_register(self, field_name, default=None):
|
||||
return self.field_to_register.get(field_name, default)
|
||||
def get_field(self, field_name, reg_value=None, reg_name=None):
|
||||
# Returns value of the register field
|
||||
if reg_name is None:
|
||||
reg_name = self.field_to_register[field_name]
|
||||
if reg_value is None:
|
||||
reg_value = self.registers.get(reg_name, 0)
|
||||
mask = self.all_fields[reg_name][field_name]
|
||||
field_value = (reg_value & mask) >> ffs(mask)
|
||||
if field_name in self.signed_fields and ((reg_value & mask)<<1) > mask:
|
||||
field_value -= (1 << field_value.bit_length())
|
||||
return field_value
|
||||
def set_field(self, field_name, field_value, reg_value=None, reg_name=None):
|
||||
# Returns register value with field bits filled with supplied value
|
||||
if reg_name is None:
|
||||
reg_name = self.field_to_register[field_name]
|
||||
if reg_value is None:
|
||||
reg_value = self.registers.get(reg_name, 0)
|
||||
mask = self.all_fields[reg_name][field_name]
|
||||
new_value = (reg_value & ~mask) | ((field_value << ffs(mask)) & mask)
|
||||
self.registers[reg_name] = new_value
|
||||
return new_value
|
||||
def set_config_field(self, config, field_name, default):
|
||||
# Allow a field to be set from the config file
|
||||
config_name = "driver_" + field_name.upper()
|
||||
reg_name = self.field_to_register[field_name]
|
||||
mask = self.all_fields[reg_name][field_name]
|
||||
maxval = mask >> ffs(mask)
|
||||
if maxval == 1:
|
||||
val = config.getboolean(config_name, default)
|
||||
elif field_name in self.signed_fields:
|
||||
val = config.getint(config_name, default,
|
||||
minval=-(maxval//2 + 1), maxval=maxval//2)
|
||||
else:
|
||||
val = config.getint(config_name, default, minval=0, maxval=maxval)
|
||||
return self.set_field(field_name, val)
|
||||
def pretty_format(self, reg_name, reg_value):
|
||||
# Provide a string description of a register
|
||||
reg_fields = self.all_fields.get(reg_name, {})
|
||||
reg_fields = sorted([(mask, name) for name, mask in reg_fields.items()])
|
||||
fields = []
|
||||
for mask, field_name in reg_fields:
|
||||
field_value = self.get_field(field_name, reg_value, reg_name)
|
||||
sval = self.field_formatters.get(field_name, str)(field_value)
|
||||
if sval and sval != "0":
|
||||
fields.append(" %s=%s" % (field_name, sval))
|
||||
return "%-11s %08x%s" % (reg_name + ":", reg_value, "".join(fields))
|
||||
def get_reg_fields(self, reg_name, reg_value):
|
||||
# Provide fields found in a register
|
||||
reg_fields = self.all_fields.get(reg_name, {})
|
||||
return {field_name: self.get_field(field_name, reg_value, reg_name)
|
||||
for field_name, mask in reg_fields.items()}
|
||||
|
||||
|
||||
######################################################################
|
||||
# Periodic error checking
|
||||
######################################################################
|
||||
|
||||
class TMCErrorCheck:
|
||||
def __init__(self, config, mcu_tmc):
|
||||
self.printer = config.get_printer()
|
||||
name_parts = config.get_name().split()
|
||||
self.stepper_name = ' '.join(name_parts[1:])
|
||||
self.mcu_tmc = mcu_tmc
|
||||
self.fields = mcu_tmc.get_fields()
|
||||
self.check_timer = None
|
||||
self.last_drv_status = self.last_status = None
|
||||
# Setup for GSTAT query
|
||||
reg_name = self.fields.lookup_register("drv_err")
|
||||
if reg_name is not None:
|
||||
self.gstat_reg_info = [0, reg_name, 0xffffffff, 0xffffffff, 0]
|
||||
else:
|
||||
self.gstat_reg_info = None
|
||||
self.clear_gstat = True
|
||||
# Setup for DRV_STATUS query
|
||||
self.irun_field = "irun"
|
||||
reg_name = "DRV_STATUS"
|
||||
mask = err_mask = cs_actual_mask = 0
|
||||
if name_parts[0] == 'tmc2130':
|
||||
# TMC2130 driver quirks
|
||||
self.clear_gstat = False
|
||||
cs_actual_mask = self.fields.all_fields[reg_name]["cs_actual"]
|
||||
elif name_parts[0] == 'tmc2660':
|
||||
# TMC2660 driver quirks
|
||||
self.irun_field = "cs"
|
||||
reg_name = "READRSP@RDSEL2"
|
||||
cs_actual_mask = self.fields.all_fields[reg_name]["se"]
|
||||
err_fields = ["ot", "s2ga", "s2gb", "s2vsa", "s2vsb"]
|
||||
warn_fields = ["otpw", "t120", "t143", "t150", "t157"]
|
||||
for f in err_fields + warn_fields:
|
||||
if f in self.fields.all_fields[reg_name]:
|
||||
mask |= self.fields.all_fields[reg_name][f]
|
||||
if f in err_fields:
|
||||
err_mask |= self.fields.all_fields[reg_name][f]
|
||||
self.drv_status_reg_info = [0, reg_name, mask, err_mask, cs_actual_mask]
|
||||
def _query_register(self, reg_info, try_clear=False):
|
||||
last_value, reg_name, mask, err_mask, cs_actual_mask = reg_info
|
||||
cleared_flags = 0
|
||||
count = 0
|
||||
while 1:
|
||||
try:
|
||||
val = self.mcu_tmc.get_register(reg_name)
|
||||
except self.printer.command_error as e:
|
||||
count += 1
|
||||
if count < 3 and str(e).startswith("Unable to read tmc uart"):
|
||||
# Allow more retries on a TMC UART read error
|
||||
reactor = self.printer.get_reactor()
|
||||
reactor.pause(reactor.monotonic() + 0.050)
|
||||
continue
|
||||
raise
|
||||
if val & mask != last_value & mask:
|
||||
fmt = self.fields.pretty_format(reg_name, val)
|
||||
logging.info("TMC '%s' reports %s", self.stepper_name, fmt)
|
||||
reg_info[0] = last_value = val
|
||||
if not val & err_mask:
|
||||
if not cs_actual_mask or val & cs_actual_mask:
|
||||
break
|
||||
irun = self.fields.get_field(self.irun_field)
|
||||
if self.check_timer is None or irun < 4:
|
||||
break
|
||||
if (self.irun_field == "irun"
|
||||
and not self.fields.get_field("ihold")):
|
||||
break
|
||||
# CS_ACTUAL field of zero - indicates a driver reset
|
||||
count += 1
|
||||
if count >= 3:
|
||||
fmt = self.fields.pretty_format(reg_name, val)
|
||||
code_key = "key505"
|
||||
m = """{"code":"%s","msg":"TMC '%s' reports error: %s"}""" % (code_key, self.stepper_name, fmt)
|
||||
raise self.printer.command_error(m)
|
||||
if try_clear and val & err_mask:
|
||||
try_clear = False
|
||||
cleared_flags |= val & err_mask
|
||||
self.mcu_tmc.set_register(reg_name, val & err_mask)
|
||||
return cleared_flags
|
||||
def _do_periodic_check(self, eventtime):
|
||||
try:
|
||||
self._query_register(self.drv_status_reg_info)
|
||||
if self.gstat_reg_info is not None:
|
||||
self._query_register(self.gstat_reg_info)
|
||||
except self.printer.command_error as e:
|
||||
self.printer.invoke_shutdown(str(e))
|
||||
return self.printer.get_reactor().NEVER
|
||||
return eventtime + 1.
|
||||
def stop_checks(self):
|
||||
if self.check_timer is None:
|
||||
return
|
||||
self.printer.get_reactor().unregister_timer(self.check_timer)
|
||||
self.check_timer = None
|
||||
def start_checks(self):
|
||||
if self.check_timer is not None:
|
||||
self.stop_checks()
|
||||
cleared_flags = 0
|
||||
self._query_register(self.drv_status_reg_info)
|
||||
if self.gstat_reg_info is not None:
|
||||
cleared_flags = self._query_register(self.gstat_reg_info,
|
||||
try_clear=self.clear_gstat)
|
||||
reactor = self.printer.get_reactor()
|
||||
curtime = reactor.monotonic()
|
||||
self.check_timer = reactor.register_timer(self._do_periodic_check,
|
||||
curtime + 1.)
|
||||
if cleared_flags:
|
||||
reset_mask = self.fields.all_fields["GSTAT"]["reset"]
|
||||
if cleared_flags & reset_mask:
|
||||
return True
|
||||
return False
|
||||
def get_status(self, eventtime=None):
|
||||
if self.check_timer is None:
|
||||
return {'drv_status': None}
|
||||
last_value, reg_name = self.drv_status_reg_info[:2]
|
||||
if last_value != self.last_drv_status:
|
||||
self.last_drv_status = last_value
|
||||
fields = self.fields.get_reg_fields(reg_name, last_value)
|
||||
fields = {n: v for n, v in fields.items() if v}
|
||||
self.last_status = {'drv_status': fields}
|
||||
return self.last_status
|
||||
|
||||
|
||||
######################################################################
|
||||
# G-Code command helpers
|
||||
######################################################################
|
||||
|
||||
class TMCCommandHelper:
|
||||
def __init__(self, config, mcu_tmc, current_helper):
|
||||
self.printer = config.get_printer()
|
||||
self.stepper_name = ' '.join(config.get_name().split()[1:])
|
||||
self.name = config.get_name().split()[-1]
|
||||
self.mcu_tmc = mcu_tmc
|
||||
self.current_helper = current_helper
|
||||
self.echeck_helper = TMCErrorCheck(config, mcu_tmc)
|
||||
self.fields = mcu_tmc.get_fields()
|
||||
self.read_registers = self.read_translate = None
|
||||
self.toff = None
|
||||
self.mcu_phase_offset = None
|
||||
self.stepper = None
|
||||
self.stepper_enable = self.printer.load_object(config, "stepper_enable")
|
||||
self.printer.register_event_handler("stepper:sync_mcu_position",
|
||||
self._handle_sync_mcu_pos)
|
||||
self.printer.register_event_handler("stepper:set_sdir_inverted",
|
||||
self._handle_sync_mcu_pos)
|
||||
self.printer.register_event_handler("klippy:mcu_identify",
|
||||
self._handle_mcu_identify)
|
||||
self.printer.register_event_handler("klippy:connect",
|
||||
self._handle_connect)
|
||||
# Set microstep config options
|
||||
TMCMicrostepHelper(config, mcu_tmc)
|
||||
# Register commands
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
gcode.register_mux_command("SET_TMC_FIELD", "STEPPER", self.name,
|
||||
self.cmd_SET_TMC_FIELD,
|
||||
desc=self.cmd_SET_TMC_FIELD_help)
|
||||
gcode.register_mux_command("INIT_TMC", "STEPPER", self.name,
|
||||
self.cmd_INIT_TMC,
|
||||
desc=self.cmd_INIT_TMC_help)
|
||||
gcode.register_mux_command("SET_TMC_CURRENT", "STEPPER", self.name,
|
||||
self.cmd_SET_TMC_CURRENT,
|
||||
desc=self.cmd_SET_TMC_CURRENT_help)
|
||||
def _init_registers(self, print_time=None):
|
||||
# Send registers
|
||||
for reg_name, val in self.fields.registers.items():
|
||||
self.mcu_tmc.set_register(reg_name, val, print_time)
|
||||
cmd_INIT_TMC_help = "Initialize TMC stepper driver registers"
|
||||
def cmd_INIT_TMC(self, gcmd):
|
||||
logging.info("INIT_TMC %s", self.name)
|
||||
print_time = self.printer.lookup_object('toolhead').get_last_move_time()
|
||||
self._init_registers(print_time)
|
||||
cmd_SET_TMC_FIELD_help = "Set a register field of a TMC driver"
|
||||
def cmd_SET_TMC_FIELD(self, gcmd):
|
||||
field_name = gcmd.get('FIELD').lower()
|
||||
reg_name = self.fields.lookup_register(field_name, None)
|
||||
if reg_name is None:
|
||||
raise gcmd.error("Unknown field name '%s'" % (field_name,))
|
||||
value = gcmd.get_int('VALUE')
|
||||
reg_val = self.fields.set_field(field_name, value)
|
||||
print_time = self.printer.lookup_object('toolhead').get_last_move_time()
|
||||
self.mcu_tmc.set_register(reg_name, reg_val, print_time)
|
||||
cmd_SET_TMC_CURRENT_help = "Set the current of a TMC driver"
|
||||
def cmd_SET_TMC_CURRENT(self, gcmd):
|
||||
ch = self.current_helper
|
||||
prev_cur, prev_hold_cur, req_hold_cur, max_cur = ch.get_current()
|
||||
run_current = gcmd.get_float('CURRENT', None, minval=0., maxval=max_cur)
|
||||
hold_current = gcmd.get_float('HOLDCURRENT', None,
|
||||
above=0., maxval=max_cur)
|
||||
if run_current is not None or hold_current is not None:
|
||||
if run_current is None:
|
||||
run_current = prev_cur
|
||||
if hold_current is None:
|
||||
hold_current = req_hold_cur
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
print_time = toolhead.get_last_move_time()
|
||||
ch.set_current(run_current, hold_current, print_time)
|
||||
prev_cur, prev_hold_cur, req_hold_cur, max_cur = ch.get_current()
|
||||
# Report values
|
||||
if prev_hold_cur is None:
|
||||
gcmd.respond_info("Run Current: %0.2fA" % (prev_cur,))
|
||||
else:
|
||||
gcmd.respond_info("Run Current: %0.2fA Hold Current: %0.2fA"
|
||||
% (prev_cur, prev_hold_cur))
|
||||
# Stepper phase tracking
|
||||
def _get_phases(self):
|
||||
return (256 >> self.fields.get_field("mres")) * 4
|
||||
def get_phase_offset(self):
|
||||
return self.mcu_phase_offset, self._get_phases()
|
||||
def _query_phase(self):
|
||||
field_name = "mscnt"
|
||||
if self.fields.lookup_register(field_name, None) is None:
|
||||
# TMC2660 uses MSTEP
|
||||
field_name = "mstep"
|
||||
reg = self.mcu_tmc.get_register(self.fields.lookup_register(field_name))
|
||||
return self.fields.get_field(field_name, reg)
|
||||
def _handle_sync_mcu_pos(self, stepper):
|
||||
if stepper.get_name() != self.stepper_name:
|
||||
return
|
||||
try:
|
||||
driver_phase = self._query_phase()
|
||||
except self.printer.command_error as e:
|
||||
logging.info("Unable to obtain tmc %s phase", self.stepper_name)
|
||||
self.mcu_phase_offset = None
|
||||
enable_line = self.stepper_enable.lookup_enable(self.stepper_name)
|
||||
if enable_line.is_motor_enabled():
|
||||
raise
|
||||
return
|
||||
if not stepper.get_dir_inverted()[0]:
|
||||
driver_phase = 1023 - driver_phase
|
||||
phases = self._get_phases()
|
||||
phase = int(float(driver_phase) / 1024 * phases + .5) % phases
|
||||
moff = (phase - stepper.get_mcu_position()) % phases
|
||||
if self.mcu_phase_offset is not None and self.mcu_phase_offset != moff:
|
||||
logging.warning("Stepper %s phase change (was %d now %d)",
|
||||
self.stepper_name, self.mcu_phase_offset, moff)
|
||||
self.mcu_phase_offset = moff
|
||||
# Stepper enable/disable tracking
|
||||
def _do_enable(self, print_time):
|
||||
try:
|
||||
if self.toff is not None:
|
||||
# Shared enable via comms handling
|
||||
self.fields.set_field("toff", self.toff)
|
||||
self._init_registers()
|
||||
did_reset = self.echeck_helper.start_checks()
|
||||
if did_reset:
|
||||
self.mcu_phase_offset = None
|
||||
# Calculate phase offset
|
||||
if self.mcu_phase_offset is not None:
|
||||
return
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
with gcode.get_mutex():
|
||||
if self.mcu_phase_offset is not None:
|
||||
return
|
||||
logging.info("Pausing toolhead to calculate %s phase offset",
|
||||
self.stepper_name)
|
||||
self.printer.lookup_object('toolhead').wait_moves()
|
||||
self._handle_sync_mcu_pos(self.stepper)
|
||||
except self.printer.command_error as e:
|
||||
self.printer.invoke_shutdown(str(e))
|
||||
def _do_disable(self, print_time):
|
||||
try:
|
||||
if self.toff is not None:
|
||||
val = self.fields.set_field("toff", 0)
|
||||
reg_name = self.fields.lookup_register("toff")
|
||||
self.mcu_tmc.set_register(reg_name, val, print_time)
|
||||
self.echeck_helper.stop_checks()
|
||||
except self.printer.command_error as e:
|
||||
self.printer.invoke_shutdown(str(e))
|
||||
def _handle_mcu_identify(self):
|
||||
# Lookup stepper object
|
||||
force_move = self.printer.lookup_object("force_move")
|
||||
self.stepper = force_move.lookup_stepper(self.stepper_name)
|
||||
# Note pulse duration and step_both_edge optimizations available
|
||||
self.stepper.setup_default_pulse_duration(.000000100, True)
|
||||
def _handle_stepper_enable(self, print_time, is_enable):
|
||||
if is_enable:
|
||||
cb = (lambda ev: self._do_enable(print_time))
|
||||
else:
|
||||
cb = (lambda ev: self._do_disable(print_time))
|
||||
self.printer.get_reactor().register_callback(cb)
|
||||
def _handle_connect(self):
|
||||
# Check if using step on both edges optimization
|
||||
pulse_duration, step_both_edge = self.stepper.get_pulse_duration()
|
||||
if step_both_edge:
|
||||
self.fields.set_field("dedge", 1)
|
||||
# Check for soft stepper enable/disable
|
||||
enable_line = self.stepper_enable.lookup_enable(self.stepper_name)
|
||||
enable_line.register_state_callback(self._handle_stepper_enable)
|
||||
if not enable_line.has_dedicated_enable():
|
||||
self.toff = self.fields.get_field("toff")
|
||||
self.fields.set_field("toff", 0)
|
||||
logging.info("Enabling TMC virtual enable for '%s'",
|
||||
self.stepper_name)
|
||||
# Send init
|
||||
try:
|
||||
self._init_registers()
|
||||
except self.printer.command_error as e:
|
||||
logging.info("TMC %s failed to init: %s", self.name, str(e))
|
||||
# get_status information export
|
||||
def get_status(self, eventtime=None):
|
||||
cpos = None
|
||||
if self.stepper is not None and self.mcu_phase_offset is not None:
|
||||
cpos = self.stepper.mcu_to_commanded_position(self.mcu_phase_offset)
|
||||
current = self.current_helper.get_current()
|
||||
res = {'mcu_phase_offset': self.mcu_phase_offset,
|
||||
'phase_offset_position': cpos,
|
||||
'run_current': current[0],
|
||||
'hold_current': current[1]}
|
||||
res.update(self.echeck_helper.get_status(eventtime))
|
||||
return res
|
||||
# DUMP_TMC support
|
||||
def setup_register_dump(self, read_registers, read_translate=None):
|
||||
self.read_registers = read_registers
|
||||
self.read_translate = read_translate
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
gcode.register_mux_command("DUMP_TMC", "STEPPER", self.name,
|
||||
self.cmd_DUMP_TMC,
|
||||
desc=self.cmd_DUMP_TMC_help)
|
||||
cmd_DUMP_TMC_help = "Read and display TMC stepper driver registers"
|
||||
def cmd_DUMP_TMC(self, gcmd):
|
||||
logging.info("DUMP_TMC %s", self.name)
|
||||
print_time = self.printer.lookup_object('toolhead').get_last_move_time()
|
||||
gcmd.respond_info("========== Write-only registers ==========")
|
||||
for reg_name, val in self.fields.registers.items():
|
||||
if reg_name not in self.read_registers:
|
||||
gcmd.respond_info(self.fields.pretty_format(reg_name, val))
|
||||
gcmd.respond_info("========== Queried registers ==========")
|
||||
for reg_name in self.read_registers:
|
||||
val = self.mcu_tmc.get_register(reg_name)
|
||||
if self.read_translate is not None:
|
||||
reg_name, val = self.read_translate(reg_name, val)
|
||||
gcmd.respond_info(self.fields.pretty_format(reg_name, val))
|
||||
|
||||
|
||||
######################################################################
|
||||
# TMC virtual pins
|
||||
######################################################################
|
||||
|
||||
# Helper class for "sensorless homing"
|
||||
class TMCVirtualPinHelper:
|
||||
def __init__(self, config, mcu_tmc):
|
||||
self.printer = config.get_printer()
|
||||
self.mcu_tmc = mcu_tmc
|
||||
self.fields = mcu_tmc.get_fields()
|
||||
if self.fields.lookup_register('diag0_stall') is not None:
|
||||
if config.get('diag0_pin', None) is not None:
|
||||
self.diag_pin = config.get('diag0_pin')
|
||||
self.diag_pin_field = 'diag0_stall'
|
||||
else:
|
||||
self.diag_pin = config.get('diag1_pin', None)
|
||||
self.diag_pin_field = 'diag1_stall'
|
||||
else:
|
||||
self.diag_pin = config.get('diag_pin', None)
|
||||
self.diag_pin_field = None
|
||||
self.mcu_endstop = None
|
||||
self.en_pwm = False
|
||||
self.pwmthrs = 0
|
||||
# Register virtual_endstop pin
|
||||
name_parts = config.get_name().split()
|
||||
ppins = self.printer.lookup_object("pins")
|
||||
ppins.register_chip("%s_%s" % (name_parts[0], name_parts[-1]), self)
|
||||
def setup_pin(self, pin_type, pin_params):
|
||||
# Validate pin
|
||||
ppins = self.printer.lookup_object('pins')
|
||||
if pin_type != 'endstop' or pin_params['pin'] != 'virtual_endstop':
|
||||
raise ppins.error("tmc virtual endstop only useful as endstop")
|
||||
if pin_params['invert'] or pin_params['pullup']:
|
||||
raise ppins.error("Can not pullup/invert tmc virtual pin")
|
||||
if self.diag_pin is None:
|
||||
raise ppins.error("tmc virtual endstop requires diag pin config")
|
||||
# Setup for sensorless homing
|
||||
reg = self.fields.lookup_register("en_pwm_mode", None)
|
||||
if reg is None:
|
||||
self.en_pwm = not self.fields.get_field("en_spreadcycle")
|
||||
self.pwmthrs = self.fields.get_field("tpwmthrs")
|
||||
else:
|
||||
self.en_pwm = self.fields.get_field("en_pwm_mode")
|
||||
self.pwmthrs = 0
|
||||
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.mcu_endstop = ppins.setup_pin('endstop', self.diag_pin)
|
||||
return self.mcu_endstop
|
||||
def handle_homing_move_begin(self, hmove):
|
||||
if self.mcu_endstop not in hmove.get_mcu_endstops():
|
||||
return
|
||||
reg = self.fields.lookup_register("en_pwm_mode", None)
|
||||
if reg is None:
|
||||
# On "stallguard4" drivers, "stealthchop" must be enabled
|
||||
tp_val = self.fields.set_field("tpwmthrs", 0)
|
||||
self.mcu_tmc.set_register("TPWMTHRS", tp_val)
|
||||
val = self.fields.set_field("en_spreadcycle", 0)
|
||||
else:
|
||||
# On earlier drivers, "stealthchop" must be disabled
|
||||
self.fields.set_field("en_pwm_mode", 0)
|
||||
val = self.fields.set_field(self.diag_pin_field, 1)
|
||||
self.mcu_tmc.set_register("GCONF", val)
|
||||
tc_val = self.fields.set_field("tcoolthrs", 0xfffff)
|
||||
self.mcu_tmc.set_register("TCOOLTHRS", tc_val)
|
||||
def handle_homing_move_end(self, hmove):
|
||||
if self.mcu_endstop not in hmove.get_mcu_endstops():
|
||||
return
|
||||
reg = self.fields.lookup_register("en_pwm_mode", None)
|
||||
if reg is None:
|
||||
tp_val = self.fields.set_field("tpwmthrs", self.pwmthrs)
|
||||
self.mcu_tmc.set_register("TPWMTHRS", tp_val)
|
||||
val = self.fields.set_field("en_spreadcycle", not self.en_pwm)
|
||||
else:
|
||||
self.fields.set_field("en_pwm_mode", self.en_pwm)
|
||||
val = self.fields.set_field(self.diag_pin_field, 0)
|
||||
self.mcu_tmc.set_register("GCONF", val)
|
||||
tc_val = self.fields.set_field("tcoolthrs", 0)
|
||||
self.mcu_tmc.set_register("TCOOLTHRS", tc_val)
|
||||
|
||||
|
||||
######################################################################
|
||||
# Config reading helpers
|
||||
######################################################################
|
||||
|
||||
# Helper to initialize the wave table from config or defaults
|
||||
def TMCWaveTableHelper(config, mcu_tmc):
|
||||
set_config_field = mcu_tmc.get_fields().set_config_field
|
||||
set_config_field(config, "mslut0", 0xAAAAB554)
|
||||
set_config_field(config, "mslut1", 0x4A9554AA)
|
||||
set_config_field(config, "mslut2", 0x24492929)
|
||||
set_config_field(config, "mslut3", 0x10104222)
|
||||
set_config_field(config, "mslut4", 0xFBFFFFFF)
|
||||
set_config_field(config, "mslut5", 0xB5BB777D)
|
||||
set_config_field(config, "mslut6", 0x49295556)
|
||||
set_config_field(config, "mslut7", 0x00404222)
|
||||
set_config_field(config, "w0", 2)
|
||||
set_config_field(config, "w1", 1)
|
||||
set_config_field(config, "w2", 1)
|
||||
set_config_field(config, "w3", 1)
|
||||
set_config_field(config, "x1", 128)
|
||||
set_config_field(config, "x2", 255)
|
||||
set_config_field(config, "x3", 255)
|
||||
set_config_field(config, "start_sin", 0)
|
||||
set_config_field(config, "start_sin90", 247)
|
||||
|
||||
# Helper to configure and query the microstep settings
|
||||
def TMCMicrostepHelper(config, mcu_tmc):
|
||||
fields = mcu_tmc.get_fields()
|
||||
stepper_name = " ".join(config.get_name().split()[1:])
|
||||
if not config.has_section(stepper_name):
|
||||
raise config.error(
|
||||
"Could not find config section '[%s]' required by tmc driver"
|
||||
% (stepper_name,))
|
||||
stepper_config = ms_config = config.getsection(stepper_name)
|
||||
if (stepper_config.get('microsteps', None, note_valid=False) is None
|
||||
and config.get('microsteps', None, note_valid=False) is not None):
|
||||
# Older config format with microsteps in tmc config section
|
||||
ms_config = config
|
||||
steps = {256: 0, 128: 1, 64: 2, 32: 3, 16: 4, 8: 5, 4: 6, 2: 7, 1: 8}
|
||||
mres = ms_config.getchoice('microsteps', steps)
|
||||
fields.set_field("mres", mres)
|
||||
fields.set_field("intpol", config.getboolean("interpolate", True))
|
||||
|
||||
# Helper to configure "stealthchop" mode
|
||||
def TMCStealthchopHelper(config, mcu_tmc, tmc_freq):
|
||||
fields = mcu_tmc.get_fields()
|
||||
en_pwm_mode = False
|
||||
velocity = config.getfloat('stealthchop_threshold', 0., minval=0.)
|
||||
if velocity:
|
||||
stepper_name = " ".join(config.get_name().split()[1:])
|
||||
sconfig = config.getsection(stepper_name)
|
||||
rotation_dist, steps_per_rotation = stepper.parse_step_distance(sconfig)
|
||||
step_dist = rotation_dist / steps_per_rotation
|
||||
step_dist_256 = step_dist / (1 << fields.get_field("mres"))
|
||||
threshold = int(tmc_freq * step_dist_256 / velocity + .5)
|
||||
fields.set_field("tpwmthrs", max(0, min(0xfffff, threshold)))
|
||||
en_pwm_mode = True
|
||||
reg = fields.lookup_register("en_pwm_mode", None)
|
||||
if reg is not None:
|
||||
fields.set_field("en_pwm_mode", en_pwm_mode)
|
||||
else:
|
||||
# TMC2208 uses en_spreadCycle
|
||||
fields.set_field("en_spreadcycle", not en_pwm_mode)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user