update for open source

This commit is contained in:
liyongjie
2024-01-09 17:33:02 +08:00
parent 9bdde73f34
commit a63fb1a716
1896 changed files with 2457352 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Package definition for the extras directory
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
+79
View File
@@ -0,0 +1,79 @@
# Support for scaling ADC values based on measured VREF and VSSA
#
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
SAMPLE_TIME = 0.001
SAMPLE_COUNT = 8
REPORT_TIME = 0.300
RANGE_CHECK_COUNT = 4
class MCU_scaled_adc:
def __init__(self, main, pin_params):
self._main = main
self._last_state = (0., 0.)
self._mcu_adc = main.mcu.setup_pin('adc', pin_params)
query_adc = main.printer.lookup_object('query_adc')
qname = main.name + ":" + pin_params['pin']
query_adc.register_adc(qname, self._mcu_adc)
self._callback = None
self.setup_minmax = self._mcu_adc.setup_minmax
self.get_mcu = self._mcu_adc.get_mcu
def _handle_callback(self, read_time, read_value):
max_adc = self._main.last_vref[1]
min_adc = self._main.last_vssa[1]
scaled_val = (read_value - min_adc) / (max_adc - min_adc)
self._last_state = (scaled_val, read_time)
self._callback(read_time, scaled_val)
def setup_adc_callback(self, report_time, callback):
self._callback = callback
self._mcu_adc.setup_adc_callback(report_time, self._handle_callback)
def get_last_value(self):
return self._last_state
class PrinterADCScaled:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[1]
self.last_vref = (0., 0.)
self.last_vssa = (0., 0.)
# Configure vref and vssa pins
self.mcu_vref = self._config_pin(config, 'vref', self.vref_callback)
self.mcu_vssa = self._config_pin(config, 'vssa', self.vssa_callback)
smooth_time = config.getfloat('smooth_time', 2., above=0.)
self.inv_smooth_time = 1. / smooth_time
self.mcu = self.mcu_vref.get_mcu()
if self.mcu is not self.mcu_vssa.get_mcu():
raise config.error("""{"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)
+315
View File
@@ -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)
+524
View File
@@ -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
+118
View File
@@ -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)
+301
View File
@@ -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)
+276
View File
@@ -0,0 +1,276 @@
# BLTouch support
#
# Copyright (C) 2018-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import probe
SIGNAL_PERIOD = 0.020
MIN_CMD_TIME = 5 * SIGNAL_PERIOD
TEST_TIME = 5 * 60.
RETRY_RESET_TIME = 1.
ENDSTOP_REST_TIME = .001
ENDSTOP_SAMPLE_TIME = .000015
ENDSTOP_SAMPLE_COUNT = 4
Commands = {
'pin_down': 0.000650, 'touch_mode': 0.001165,
'pin_up': 0.001475, 'self_test': 0.001780, 'reset': 0.002190,
'set_5V_output_mode' : 0.001988, 'set_OD_output_mode' : 0.002091,
'output_mode_store' : 0.001884,
}
# BLTouch "endstop" wrapper
class BLTouchEndstopWrapper:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.printer.register_event_handler('klippy:mcu_identify',
self.handle_mcu_identify)
self.position_endstop = config.getfloat('z_offset', minval=0.)
self.stow_on_each_sample = config.getboolean('stow_on_each_sample',
True)
self.probe_touch_mode = config.getboolean('probe_with_touch_mode',
False)
# Create a pwm object to handle the control pin
ppins = self.printer.lookup_object('pins')
self.mcu_pwm = ppins.setup_pin('pwm', config.get('control_pin'))
self.mcu_pwm.setup_max_duration(0.)
self.mcu_pwm.setup_cycle_time(SIGNAL_PERIOD)
# Command timing
self.next_cmd_time = self.action_end_time = 0.
self.finish_home_complete = self.wait_trigger_complete = None
# Create an "endstop" object to handle the sensor pin
pin = config.get('sensor_pin')
pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True)
mcu = pin_params['chip']
self.mcu_endstop = mcu.setup_pin('endstop', pin_params)
# output mode
omodes = {'5V': '5V', 'OD': 'OD', None: None}
self.output_mode = config.getchoice('set_output_mode', omodes, None)
# Setup for sensor test
self.next_test_time = 0.
self.pin_up_not_triggered = config.getboolean(
'pin_up_reports_not_triggered', True)
self.pin_up_touch_triggered = config.getboolean(
'pin_up_touch_mode_reports_triggered', True)
# Calculate pin move time
self.pin_move_time = config.getfloat('pin_move_time', 0.680, above=0.)
# Wrappers
self.get_mcu = self.mcu_endstop.get_mcu
self.add_stepper = self.mcu_endstop.add_stepper
self.get_steppers = self.mcu_endstop.get_steppers
self.home_wait = self.mcu_endstop.home_wait
self.query_endstop = self.mcu_endstop.query_endstop
# Register BLTOUCH_DEBUG command
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command("BLTOUCH_DEBUG", self.cmd_BLTOUCH_DEBUG,
desc=self.cmd_BLTOUCH_DEBUG_help)
self.gcode.register_command("BLTOUCH_STORE", self.cmd_BLTOUCH_STORE,
desc=self.cmd_BLTOUCH_STORE_help)
# multi probes state
self.multi = 'OFF'
def handle_mcu_identify(self):
kin = self.printer.lookup_object('toolhead').get_kinematics()
for stepper in kin.get_steppers():
if stepper.is_active_axis('z'):
self.add_stepper(stepper)
def handle_connect(self):
self.sync_mcu_print_time()
self.next_cmd_time += 0.200
self.set_output_mode(self.output_mode)
try:
self.raise_probe()
self.verify_raise_probe()
except self.printer.command_error as e:
logging.warning("BLTouch raise probe error: %s", str(e))
def sync_mcu_print_time(self):
curtime = self.printer.get_reactor().monotonic()
est_time = self.mcu_pwm.get_mcu().estimated_print_time(curtime)
self.next_cmd_time = max(self.next_cmd_time, est_time + MIN_CMD_TIME)
def sync_print_time(self):
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
if self.next_cmd_time > print_time:
toolhead.dwell(self.next_cmd_time - print_time)
else:
self.next_cmd_time = print_time
def send_cmd(self, cmd, duration=MIN_CMD_TIME):
# Translate duration to ticks to avoid any secondary mcu clock skew
mcu = self.mcu_pwm.get_mcu()
cmd_clock = mcu.print_time_to_clock(self.next_cmd_time)
pulse = int((duration - MIN_CMD_TIME) / SIGNAL_PERIOD) * SIGNAL_PERIOD
cmd_clock += mcu.seconds_to_clock(max(MIN_CMD_TIME, pulse))
end_time = mcu.clock_to_print_time(cmd_clock)
# Schedule command followed by PWM disable
self.mcu_pwm.set_pwm(self.next_cmd_time, Commands[cmd] / SIGNAL_PERIOD)
self.mcu_pwm.set_pwm(end_time, 0.)
# Update time tracking
self.action_end_time = self.next_cmd_time + duration
self.next_cmd_time = max(self.action_end_time, end_time + MIN_CMD_TIME)
def verify_state(self, triggered):
# Perform endstop check to verify bltouch reports desired state
self.mcu_endstop.home_start(self.action_end_time, ENDSTOP_SAMPLE_TIME,
ENDSTOP_SAMPLE_COUNT, ENDSTOP_REST_TIME,
triggered=triggered)
trigger_time = self.mcu_endstop.home_wait(self.action_end_time + 0.100)
return trigger_time > 0.
def raise_probe(self):
self.sync_mcu_print_time()
if not self.pin_up_not_triggered:
self.send_cmd('reset')
self.send_cmd('pin_up', duration=self.pin_move_time)
def verify_raise_probe(self):
if not self.pin_up_not_triggered:
# No way to verify raise attempt
return
for retry in range(3):
success = self.verify_state(False)
if success:
# The "probe raised" test completed successfully
break
if retry >= 2:
raise self.printer.command_error(
'{"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
+480
View File
@@ -0,0 +1,480 @@
# Support for i2c based temperature sensors
#
# Copyright (C) 2020 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import bus
REPORT_TIME = .8
BME280_CHIP_ADDR = 0x76
BME280_REGS = {
'RESET': 0xE0, 'CTRL_HUM': 0xF2,
'STATUS': 0xF3, 'CTRL_MEAS': 0xF4, 'CONFIG': 0xF5,
'PRESSURE_MSB': 0xF7, 'PRESSURE_LSB': 0xF8, 'PRESSURE_XLSB': 0xF9,
'TEMP_MSB': 0xFA, 'TEMP_LSB': 0xFB, 'TEMP_XLSB': 0xFC,
'HUM_MSB': 0xFD, 'HUM_LSB': 0xFE, 'CAL_1': 0x88, 'CAL_2': 0xE1
}
BME680_REGS = {
'RESET': 0xE0, 'CTRL_HUM': 0x72, 'CTRL_GAS_1': 0x71, 'CTRL_GAS_0': 0x70,
'GAS_WAIT_0': 0x64, 'RES_HEAT_0': 0x5A, 'IDAC_HEAT_0': 0x50,
'STATUS': 0x73, 'EAS_STATUS_0': 0x1D, 'CTRL_MEAS': 0x74, 'CONFIG': 0x75,
'GAS_R_LSB': 0x2B, 'GAS_R_MSB': 0x2A,
'PRESSURE_MSB': 0x1F, 'PRESSURE_LSB': 0x20, 'PRESSURE_XLSB': 0x21,
'TEMP_MSB': 0x22, 'TEMP_LSB': 0x23, 'TEMP_XLSB': 0x24,
'HUM_MSB': 0x25, 'HUM_LSB': 0x26, 'CAL_1': 0x88, 'CAL_2': 0xE1,
'RES_HEAT_VAL': 0x00, 'RES_HEAT_RANGE': 0x02, 'RANGE_SWITCHING_ERROR': 0x04
}
BME680_GAS_CONSTANTS = {
0: (1., 8000000.),
1: (1., 4000000.),
2: (1., 2000000.),
3: (1., 1000000.),
4: (1., 499500.4995),
5: (0.99, 248262.1648),
6: (1., 125000.),
7: (0.992, 63004.03226),
8: (1., 31281.28128),
9: (1., 15625.),
10: (0.998, 7812.5),
11: (0.995, 3906.25),
12: (1., 1953.125),
13: (0.99, 976.5625),
14: (1., 488.28125),
15: (1., 244.140625)
}
STATUS_MEASURING = 1 << 3
STATUS_IM_UPDATE = 1
MODE = 1
RUN_GAS = 1 << 4
NB_CONV_0 = 0
EAS_NEW_DATA = 1 << 7
GAS_DONE = 1 << 6
MEASURE_DONE = 1 << 5
RESET_CHIP_VALUE = 0xB6
BME_CHIPS = {
0x58: 'BMP280', 0x60: 'BME280', 0x61: 'BME680'
}
BME_CHIP_ID_REG = 0xD0
def get_twos_complement(val, bit_size):
if val & (1 << (bit_size - 1)):
val -= (1 << bit_size)
return val
def get_unsigned_short(bits):
return bits[1] << 8 | bits[0]
def get_signed_short(bits):
val = get_unsigned_short(bits)
return get_twos_complement(val, 16)
def get_signed_byte(bits):
return get_twos_complement(bits, 8)
class BME280:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.reactor = self.printer.get_reactor()
self.i2c = bus.MCU_I2C_from_config(
config, default_addr=BME280_CHIP_ADDR, default_speed=100000)
self.mcu = self.i2c.get_mcu()
self.iir_filter = config.getint('bme280_iir_filter', 1)
self.os_temp = config.getint('bme280_oversample_temp', 2)
self.os_hum = config.getint('bme280_oversample_hum', 2)
self.os_pres = config.getint('bme280_oversample_pressure', 2)
self.gas_heat_temp = config.getint('bme280_gas_target_temp', 320)
self.gas_heat_duration = config.getint('bme280_gas_heat_duration', 150)
logging.info("BMxx80: Oversampling: Temp %dx Humid %dx Pressure %dx" % (
pow(2, self.os_temp - 1), pow(2, self.os_hum - 1),
pow(2, self.os_pres - 1)))
logging.info("BMxx80: IIR: %dx" % (pow(2, self.iir_filter) - 1))
self.temp = self.pressure = self.humidity = self.gas = self.t_fine = 0.
self.min_temp = self.max_temp = self.range_switching_error = 0.
self.max_sample_time = None
self.dig = self.sample_timer = None
self.chip_type = 'BMP280'
self.chip_registers = BME280_REGS
self.printer.add_object("bme280 " + self.name, self)
if self.printer.get_start_args().get('debugoutput') is not None:
return
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
def handle_connect(self):
self._init_bmxx80()
self.reactor.update_timer(self.sample_timer, self.reactor.NOW)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
def setup_callback(self, cb):
self._callback = cb
def get_report_time_delta(self):
return REPORT_TIME
def _init_bmxx80(self):
def read_calibration_data_bmp280(calib_data_1):
dig = {}
dig['T1'] = get_unsigned_short(calib_data_1[0:2])
dig['T2'] = get_signed_short(calib_data_1[2:4])
dig['T3'] = get_signed_short(calib_data_1[4:6])
dig['P1'] = get_unsigned_short(calib_data_1[6:8])
dig['P2'] = get_signed_short(calib_data_1[8:10])
dig['P3'] = get_signed_short(calib_data_1[10:12])
dig['P4'] = get_signed_short(calib_data_1[12:14])
dig['P5'] = get_signed_short(calib_data_1[14:16])
dig['P6'] = get_signed_short(calib_data_1[16:18])
dig['P7'] = get_signed_short(calib_data_1[18:20])
dig['P8'] = get_signed_short(calib_data_1[20:22])
dig['P9'] = get_signed_short(calib_data_1[22:24])
return dig
def read_calibration_data_bme280(calib_data_1, calib_data_2):
dig = read_calibration_data_bmp280(calib_data_1)
dig['H1'] = calib_data_1[25] & 0xFF
dig['H2'] = get_signed_short(calib_data_2[0:2])
dig['H3'] = calib_data_2[2] & 0xFF
dig['H4'] = get_twos_complement(
(calib_data_2[3] << 4) | (calib_data_2[4] & 0x0F), 12)
dig['H5'] = get_twos_complement(
(calib_data_2[5] << 4) | ((calib_data_2[4] & 0xF0) >> 4), 12)
dig['H6'] = get_twos_complement(calib_data_2[6], 8)
return dig
def read_calibration_data_bme680(calib_data_1, calib_data_2):
dig = {}
dig['T1'] = get_unsigned_short(calib_data_2[8:10])
dig['T2'] = get_signed_short(calib_data_1[2:4])
dig['T3'] = get_signed_byte(calib_data_1[4])
dig['P1'] = get_unsigned_short(calib_data_1[6:8])
dig['P2'] = get_signed_short(calib_data_1[8:10])
dig['P3'] = calib_data_1[10]
dig['P4'] = get_signed_short(calib_data_1[12:14])
dig['P5'] = get_signed_short(calib_data_1[14:16])
dig['P6'] = get_signed_byte(calib_data_1[17])
dig['P7'] = get_signed_byte(calib_data_1[16])
dig['P8'] = get_signed_short(calib_data_1[20:22])
dig['P9'] = get_signed_short(calib_data_1[22:24])
dig['P10'] = calib_data_1[24]
dig['H1'] = get_twos_complement(
(calib_data_2[2] << 4) | (calib_data_2[1] & 0x0F), 12)
dig['H2'] = get_twos_complement(
(calib_data_2[0] << 4) | ((calib_data_2[1] & 0xF0) >> 4), 12)
dig['H3'] = get_signed_byte(calib_data_2[3])
dig['H4'] = get_signed_byte(calib_data_2[4])
dig['H5'] = get_signed_byte(calib_data_2[5])
dig['H6'] = calib_data_2[6]
dig['H7'] = get_signed_byte(calib_data_2[7])
dig['G1'] = get_signed_byte(calib_data_2[12])
dig['G2'] = get_signed_short(calib_data_2[10:12])
dig['G3'] = get_signed_byte(calib_data_2[13])
return dig
chip_id = self.read_id()
if chip_id not in BME_CHIPS.keys():
logging.info("bme280: Unknown Chip ID received %#x" % chip_id)
else:
self.chip_type = BME_CHIPS[chip_id]
logging.info("bme280: Found Chip %s at %#x" % (
self.chip_type, self.i2c.i2c_address))
# Reset chip
self.write_register('RESET', [RESET_CHIP_VALUE])
self.reactor.pause(self.reactor.monotonic() + .5)
# Make sure non-volatile memory has been copied to registers
status = self.read_register('STATUS', 1)[0]
while status & STATUS_IM_UPDATE:
self.reactor.pause(self.reactor.monotonic() + .01)
status = self.read_register('STATUS', 1)[0]
if self.chip_type == 'BME680':
self.max_sample_time = 0.5
self.sample_timer = self.reactor.register_timer(self._sample_bme680)
self.chip_registers = BME680_REGS
else:
self.max_sample_time = \
(1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575)
+ ((2.3 * self.os_hum) + .575)) / 1000
self.sample_timer = self.reactor.register_timer(self._sample_bme280)
self.chip_registers = BME280_REGS
if self.chip_type in ('BME680', 'BME280'):
self.write_register('CONFIG', (self.iir_filter & 0x07) << 2)
# Read out and calculate the trimming parameters
cal_1 = self.read_register('CAL_1', 26)
cal_2 = self.read_register('CAL_2', 16)
if self.chip_type == 'BME280':
self.dig = read_calibration_data_bme280(cal_1, cal_2)
elif self.chip_type == 'BMP280':
self.dig = read_calibration_data_bmp280(cal_1)
elif self.chip_type == 'BME680':
self.dig = read_calibration_data_bme680(cal_1, cal_2)
def _sample_bme280(self, eventtime):
# Enter forced mode
if self.chip_type == 'BME280':
self.write_register('CTRL_HUM', self.os_hum)
meas = self.os_temp << 5 | self.os_pres << 2 | MODE
self.write_register('CTRL_MEAS', meas)
try:
# wait until results are ready
status = self.read_register('STATUS', 1)[0]
while status & STATUS_MEASURING:
self.reactor.pause(
self.reactor.monotonic() + self.max_sample_time)
status = self.read_register('STATUS', 1)[0]
if self.chip_type == 'BME280':
data = self.read_register('PRESSURE_MSB', 8)
elif self.chip_type == 'BMP280':
data = self.read_register('PRESSURE_MSB', 6)
else:
return self.reactor.NEVER
except Exception:
logging.exception("BME280: Error reading data")
self.temp = self.pressure = self.humidity = .0
return self.reactor.NEVER
temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
self.temp = self._compensate_temp(temp_raw)
pressure_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
self.pressure = self._compensate_pressure_bme280(pressure_raw) / 100.
if self.chip_type == 'BME280':
humid_raw = (data[6] << 8) | data[7]
self.humidity = self._compensate_humidity_bme280(humid_raw)
if self.temp < self.min_temp or self.temp > self.max_temp:
self.printer.invoke_shutdown(
"BME280 temperature %0.1f outside range of %0.1f:%.01f"
% (self.temp, self.min_temp, self.max_temp))
measured_time = self.reactor.monotonic()
self._callback(self.mcu.estimated_print_time(measured_time), self.temp)
return measured_time + REPORT_TIME
def _sample_bme680(self, eventtime):
self.write_register('CTRL_HUM', self.os_hum & 0x07)
meas = self.os_temp << 5 | self.os_pres << 2
self.write_register('CTRL_MEAS', [meas])
gas_wait_0 = self._calculate_gas_heater_duration(self.gas_heat_duration)
self.write_register('GAS_WAIT_0', [gas_wait_0])
res_heat_0 = self._calculate_gas_heater_resistance(self.gas_heat_temp)
self.write_register('RES_HEAT_0', [res_heat_0])
gas_config = RUN_GAS | NB_CONV_0
self.write_register('CTRL_GAS_1', [gas_config])
def data_ready(stat):
new_data = (stat & EAS_NEW_DATA)
gas_done = not (stat & GAS_DONE)
meas_done = not (stat & MEASURE_DONE)
return new_data and gas_done and meas_done
# Enter forced mode
meas = meas | MODE
self.write_register('CTRL_MEAS', meas)
try:
# wait until results are ready
status = self.read_register('EAS_STATUS_0', 1)[0]
while not data_ready(status):
self.reactor.pause(
self.reactor.monotonic() + self.max_sample_time)
status = self.read_register('EAS_STATUS_0', 1)[0]
data = self.read_register('PRESSURE_MSB', 8)
gas_data = self.read_register('GAS_R_MSB', 2)
except Exception:
logging.exception("BME680: Error reading data")
self.temp = self.pressure = self.humidity = self.gas = .0
return self.reactor.NEVER
temp_raw = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
if temp_raw != 0x80000:
self.temp = self._compensate_temp(temp_raw)
pressure_raw = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
if pressure_raw != 0x80000:
self.pressure = self._compensate_pressure_bme680(
pressure_raw) / 100.
humid_raw = (data[6] << 8) | data[7]
self.humidity = self._compensate_humidity_bme680(humid_raw)
gas_valid = ((gas_data[1] & 0x20) == 0x20)
if gas_valid:
gas_heater_stable = ((gas_data[1] & 0x10) == 0x10)
if not gas_heater_stable:
logging.warning("BME680: Gas heater didn't reach target")
gas_raw = (gas_data[0] << 2) | ((gas_data[1] & 0xC0) >> 6)
gas_range = (gas_data[1] & 0x0F)
self.gas = self._compensate_gas(gas_raw, gas_range)
if self.temp < self.min_temp or self.temp > self.max_temp:
self.printer.invoke_shutdown(
"BME680 temperature %0.1f outside range of %0.1f:%.01f"
% (self.temp, self.min_temp, self.max_temp))
measured_time = self.reactor.monotonic()
self._callback(self.mcu.estimated_print_time(measured_time), self.temp)
return measured_time + REPORT_TIME * 4
def _compensate_temp(self, raw_temp):
dig = self.dig
var1 = ((raw_temp / 16384. - (dig['T1'] / 1024.)) * dig['T2'])
var2 = (
((raw_temp / 131072.) - (dig['T1'] / 8192.)) *
((raw_temp / 131072.) - (dig['T1'] / 8192.)) * dig['T3'])
self.t_fine = var1 + var2
return self.t_fine / 5120.0
def _compensate_pressure_bme280(self, raw_pressure):
dig = self.dig
t_fine = self.t_fine
var1 = t_fine / 2. - 64000.
var2 = var1 * var1 * dig['P6'] / 32768.
var2 = var2 + var1 * dig['P5'] * 2.
var2 = var2 / 4. + (dig['P4'] * 65536.)
var1 = (dig['P3'] * var1 * var1 / 524288. + dig['P2'] * var1) / 524288.
var1 = (1. + var1 / 32768.) * dig['P1']
if var1 == 0:
return 0.
else:
pressure = 1048576.0 - raw_pressure
pressure = ((pressure - var2 / 4096.) * 6250.) / var1
var1 = dig['P9'] * pressure * pressure / 2147483648.
var2 = pressure * dig['P8'] / 32768.
return pressure + (var1 + var2 + dig['P7']) / 16.
def _compensate_pressure_bme680(self, raw_pressure):
dig = self.dig
t_fine = self.t_fine
var1 = t_fine / 2. - 64000.
var2 = var1 * var1 * dig['P6'] / 131072.
var2 = var2 + var1 * dig['P5'] * 2.
var2 = var2 / 4. + (dig['P4'] * 65536.)
var1 = (dig['P3'] * var1 * var1 / 16384. + dig['P2'] * var1) / 524288.
var1 = (1. + var1 / 32768.) * dig['P1']
if var1 == 0:
return 0.
else:
pressure = 1048576.0 - raw_pressure
pressure = ((pressure - var2 / 4096.) * 6250.) / var1
var1 = dig['P9'] * pressure * pressure / 2147483648.
var2 = pressure * dig['P8'] / 32768.
var3 = (pressure / 256.) * (pressure / 256.) * (pressure / 256.) * (
dig['P10'] / 131072.)
return pressure + (var1 + var2 + var3 + (dig['P7'] * 128.)) / 16.
def _compensate_humidity_bme280(self, raw_humidity):
dig = self.dig
t_fine = self.t_fine
humidity = t_fine - 76800.
h1 = (
raw_humidity - (
dig['H4'] * 64. + dig['H5'] / 16384. * humidity))
h2 = (dig['H2'] / 65536. * (1. + dig['H6'] / 67108864. * humidity *
(1. + dig['H3'] / 67108864. * humidity)))
humidity = h1 * h2
humidity = humidity * (1. - dig['H1'] * humidity / 524288.)
return min(100., max(0., humidity))
def _compensate_humidity_bme680(self, raw_humidity):
dig = self.dig
temp_comp = self.temp
var1 = raw_humidity - (
(dig['H1'] * 16.) + ((dig['H3'] / 2.) * temp_comp))
var2 = var1 * ((dig['H2'] / 262144.) *
(1. + ((dig['H4'] / 16384.) * temp_comp) +
((dig['H5'] / 1048576.) * temp_comp * temp_comp)))
var3 = dig['H6'] / 16384.
var4 = dig['H7'] / 2097152.
humidity = var2 + ((var3 + (var4 * temp_comp)) * var2 * var2)
return min(100., max(0., humidity))
def _compensate_gas(self, gas_raw, gas_range):
gas_switching_error = self.read_register('RANGE_SWITCHING_ERROR', 1)[0]
var1 = (1340. + 5. * gas_switching_error) * \
BME680_GAS_CONSTANTS[gas_range][0]
gas = var1 * BME680_GAS_CONSTANTS[gas_range][1] / (
gas_raw - 512. + var1)
return gas
def _calculate_gas_heater_resistance(self, target_temp):
amb_temp = self.temp
heater_data = self.read_register('RES_HEAT_VAL', 3)
res_heat_val = get_signed_byte(heater_data[0])
res_heat_range = (heater_data[2] & 0x30) >> 4
dig = self.dig
var1 = (dig['G1'] / 16.) + 49.
var2 = ((dig['G2'] / 32768.) * 0.0005) + 0.00235
var3 = dig['G3'] / 1024.
var4 = var1 * (1. + (var2 * target_temp))
var5 = var4 + (var3 * amb_temp)
res_heat = (3.4 * ((var5 * (4. / (4. + res_heat_range))
* (1. / (1. + (res_heat_val * 0.002)))) - 25))
return int(res_heat)
def _calculate_gas_heater_duration(self, duration_ms):
if duration_ms >= 4032:
duration_reg = 0xff
else:
factor = 0
while duration_ms > 0x3F:
duration_ms //= 4
factor += 1
duration_reg = duration_ms + (factor * 64)
return duration_reg
def read_id(self):
# read chip id register
regs = [BME_CHIP_ID_REG]
params = self.i2c.i2c_read(regs, 1)
return bytearray(params['response'])[0]
def read_register(self, reg_name, read_len):
# read a single register
regs = [self.chip_registers[reg_name]]
params = self.i2c.i2c_read(regs, read_len)
return bytearray(params['response'])
def write_register(self, reg_name, data):
if type(data) is not list:
data = [data]
reg = self.chip_registers[reg_name]
data.insert(0, reg)
self.i2c.i2c_write(data)
def get_status(self, eventtime):
data = {
'temperature': round(self.temp, 2),
'pressure': self.pressure
}
if self.chip_type in ('BME280', 'BME680'):
data['humidity'] = self.humidity
if self.chip_type == 'BME680':
data['gas'] = self.gas
return data
def load_config(config):
# Register sensor
pheaters = config.get_printer().load_object(config, "heaters")
pheaters.add_sensor_factory("BME280", BME280)
+27
View File
@@ -0,0 +1,27 @@
# Support for custom board pin aliases
#
# Copyright (C) 2019-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class PrinterBoardAliases:
def __init__(self, config):
ppins = config.get_printer().lookup_object('pins')
mcu_names = config.getlist('mcu', ('mcu',))
pin_resolvers = [ppins.get_pin_resolver(n) for n in mcu_names]
options = ["aliases"] + config.get_prefix_options("aliases_")
for opt in options:
aliases = config.getlists(opt, seps=('=', ','), count=2)
for name, value in aliases:
if value.startswith('<') and value.endswith('>'):
for pin_resolver in pin_resolvers:
pin_resolver.reserve_pin(name, value)
else:
for pin_resolver in pin_resolvers:
pin_resolver.alias_pin(name, value)
def load_config(config):
return PrinterBoardAliases(config)
def load_config_prefix(config):
return PrinterBoardAliases(config)
+252
View File
@@ -0,0 +1,252 @@
# Helper code for SPI and I2C bus communication
#
# Copyright (C) 2018,2019 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import mcu
def resolve_bus_name(mcu, param, bus):
# Find enumerations for the given bus
enumerations = mcu.get_enumerations()
enums = enumerations.get(param, enumerations.get('bus'))
if enums is None:
if bus is None:
return 0
return bus
# Verify bus is a valid enumeration
ppins = mcu.get_printer().lookup_object("pins")
mcu_name = mcu.get_name()
if bus is None:
rev_enums = {v: k for k, v in enums.items()}
if 0 not in rev_enums:
raise ppins.error("""{"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)
+306
View File
@@ -0,0 +1,306 @@
# Support for button detection and callbacks
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
######################################################################
# Button state tracking
######################################################################
QUERY_TIME = .002
RETRANSMIT_COUNT = 50
class MCU_buttons:
def __init__(self, printer, mcu):
self.reactor = printer.get_reactor()
self.mcu = mcu
self.mcu.register_config_callback(self.build_config)
self.pin_list = []
self.callbacks = []
self.invert = self.last_button = 0
self.ack_cmd = None
self.ack_count = 0
def setup_buttons(self, pins, callback):
mask = 0
shift = len(self.pin_list)
for pin_params in pins:
if pin_params['invert']:
self.invert |= 1 << len(self.pin_list)
mask |= 1 << len(self.pin_list)
self.pin_list.append((pin_params['pin'], pin_params['pullup']))
self.callbacks.append((mask, shift, callback))
def build_config(self):
if not self.pin_list:
return
self.oid = self.mcu.create_oid()
self.mcu.add_config_cmd("config_buttons oid=%d button_count=%d" % (
self.oid, len(self.pin_list)))
for i, (pin, pull_up) in enumerate(self.pin_list):
self.mcu.add_config_cmd(
"buttons_add oid=%d pos=%d pin=%s pull_up=%d" % (
self.oid, i, pin, pull_up), is_init=True)
cmd_queue = self.mcu.alloc_command_queue()
self.ack_cmd = self.mcu.lookup_command(
"buttons_ack oid=%c count=%c", cq=cmd_queue)
clock = self.mcu.get_query_slot(self.oid)
rest_ticks = self.mcu.seconds_to_clock(QUERY_TIME)
self.mcu.add_config_cmd(
"buttons_query oid=%d clock=%d"
" rest_ticks=%d retransmit_count=%d invert=%d" % (
self.oid, clock, rest_ticks, RETRANSMIT_COUNT,
self.invert), is_init=True)
self.mcu.register_response(self.handle_buttons_state,
"buttons_state", self.oid)
def handle_buttons_state(self, params):
# Expand the message ack_count from 8-bit
ack_count = self.ack_count
ack_diff = (ack_count - params['ack_count']) & 0xff
if ack_diff & 0x80:
ack_diff -= 0x100
msg_ack_count = ack_count - ack_diff
# Determine new buttons
buttons = bytearray(params['state'])
new_count = msg_ack_count + len(buttons) - self.ack_count
if new_count <= 0:
return
new_buttons = buttons[-new_count:]
# Send ack to MCU
self.ack_cmd.send([self.oid, new_count])
self.ack_count += new_count
# Call self.handle_button() with this event in main thread
for nb in new_buttons:
self.reactor.register_async_callback(
(lambda e, s=self, b=nb: s.handle_button(e, b)))
def handle_button(self, eventtime, button):
button ^= self.invert
changed = button ^ self.last_button
for mask, shift, callback in self.callbacks:
if changed & mask:
callback(eventtime, (button & mask) >> shift)
self.last_button = button
######################################################################
# ADC button tracking
######################################################################
ADC_REPORT_TIME = 0.015
ADC_DEBOUNCE_TIME = 0.025
ADC_SAMPLE_TIME = 0.001
ADC_SAMPLE_COUNT = 6
class MCU_ADC_buttons:
def __init__(self, printer, pin, pullup):
self.reactor = printer.get_reactor()
self.buttons = []
self.last_button = None
self.last_pressed = None
self.last_debouncetime = 0
self.pullup = pullup
self.pin = pin
self.min_value = 999999999999.9
self.max_value = 0.
ppins = printer.lookup_object('pins')
self.mcu_adc = ppins.setup_pin('adc', self.pin)
self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback)
query_adc = printer.lookup_object('query_adc')
query_adc.register_adc('adc_button:' + pin.strip(), self.mcu_adc)
def setup_button(self, min_value, max_value, callback):
self.min_value = min(self.min_value, min_value)
self.max_value = max(self.max_value, max_value)
self.buttons.append((min_value, max_value, callback))
def adc_callback(self, read_time, read_value):
adc = max(.00001, min(.99999, read_value))
value = self.pullup * adc / (1.0 - adc)
# Determine button pressed
btn = None
if self.min_value <= value <= self.max_value:
for i, (min_value, max_value, cb) in enumerate(self.buttons):
if min_value < value < max_value:
btn = i
break
# If the button changed, due to noise or pressing:
if btn != self.last_button:
# reset the debouncing timer
self.last_debouncetime = read_time
# button debounce check & new button pressed
if ((read_time - self.last_debouncetime) >= ADC_DEBOUNCE_TIME
and self.last_button == btn and self.last_pressed != btn):
# release last_pressed
if self.last_pressed is not None:
self.call_button(self.last_pressed, False)
self.last_pressed = None
if btn is not None:
self.call_button(btn, True)
self.last_pressed = btn
self.last_button = btn
def call_button(self, button, state):
minval, maxval, callback = self.buttons[button]
self.reactor.register_async_callback(
(lambda e, cb=callback, s=state: cb(e, s)))
######################################################################
# Rotary Encoders
######################################################################
# Rotary encoder handler https://github.com/brianlow/Rotary
# Copyright 2011 Ben Buxton (bb@cactii.net).
# Licenced under the GNU GPL Version 3.
class BaseRotaryEncoder:
R_START = 0x0
R_DIR_CW = 0x10
R_DIR_CCW = 0x20
R_DIR_MSK = 0x30
def __init__(self, cw_callback, ccw_callback):
self.cw_callback = cw_callback
self.ccw_callback = ccw_callback
self.encoder_state = self.R_START
def encoder_callback(self, eventtime, state):
es = self.ENCODER_STATES[self.encoder_state & 0xf][state & 0x3]
self.encoder_state = es
if es & self.R_DIR_MSK == self.R_DIR_CW:
self.cw_callback(eventtime)
elif es & self.R_DIR_MSK == self.R_DIR_CCW:
self.ccw_callback(eventtime)
class FullStepRotaryEncoder(BaseRotaryEncoder):
R_CW_FINAL = 0x1
R_CW_BEGIN = 0x2
R_CW_NEXT = 0x3
R_CCW_BEGIN = 0x4
R_CCW_FINAL = 0x5
R_CCW_NEXT = 0x6
# Use the full-step state table (emits a code at 00 only)
ENCODER_STATES = (
# R_START
(BaseRotaryEncoder.R_START, R_CW_BEGIN, R_CCW_BEGIN,
BaseRotaryEncoder.R_START),
# R_CW_FINAL
(R_CW_NEXT, BaseRotaryEncoder.R_START, R_CW_FINAL,
BaseRotaryEncoder.R_START | BaseRotaryEncoder.R_DIR_CW),
# R_CW_BEGIN
(R_CW_NEXT, R_CW_BEGIN, BaseRotaryEncoder.R_START,
BaseRotaryEncoder.R_START),
# R_CW_NEXT
(R_CW_NEXT, R_CW_BEGIN, R_CW_FINAL, BaseRotaryEncoder.R_START),
# R_CCW_BEGIN
(R_CCW_NEXT, BaseRotaryEncoder.R_START, R_CCW_BEGIN,
BaseRotaryEncoder.R_START),
# R_CCW_FINAL
(R_CCW_NEXT, R_CCW_FINAL, BaseRotaryEncoder.R_START,
BaseRotaryEncoder.R_START | BaseRotaryEncoder.R_DIR_CCW),
# R_CCW_NEXT
(R_CCW_NEXT, R_CCW_FINAL, R_CCW_BEGIN, BaseRotaryEncoder.R_START)
)
class HalfStepRotaryEncoder(BaseRotaryEncoder):
# Use the half-step state table (emits a code at 00 and 11)
R_CCW_BEGIN = 0x1
R_CW_BEGIN = 0x2
R_START_M = 0x3
R_CW_BEGIN_M = 0x4
R_CCW_BEGIN_M = 0x5
ENCODER_STATES = (
# R_START (00)
(R_START_M, R_CW_BEGIN, R_CCW_BEGIN, BaseRotaryEncoder.R_START),
# R_CCW_BEGIN
(R_START_M | BaseRotaryEncoder.R_DIR_CCW, BaseRotaryEncoder.R_START,
R_CCW_BEGIN, BaseRotaryEncoder.R_START),
# R_CW_BEGIN
(R_START_M | BaseRotaryEncoder.R_DIR_CW, R_CW_BEGIN,
BaseRotaryEncoder.R_START, BaseRotaryEncoder.R_START),
# R_START_M (11)
(R_START_M, R_CCW_BEGIN_M, R_CW_BEGIN_M, BaseRotaryEncoder.R_START),
# R_CW_BEGIN_M
(R_START_M, R_START_M, R_CW_BEGIN_M,
BaseRotaryEncoder.R_START | BaseRotaryEncoder.R_DIR_CW),
# R_CCW_BEGIN_M
(R_START_M, R_CCW_BEGIN_M, R_START_M,
BaseRotaryEncoder.R_START | BaseRotaryEncoder.R_DIR_CCW),
)
######################################################################
# Button registration code
######################################################################
class PrinterButtons:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.load_object(config, 'query_adc')
self.mcu_buttons = {}
self.adc_buttons = {}
def register_adc_button(self, pin, min_val, max_val, pullup, callback):
adc_buttons = self.adc_buttons.get(pin)
if adc_buttons is None:
self.adc_buttons[pin] = adc_buttons = MCU_ADC_buttons(
self.printer, pin, pullup)
adc_buttons.setup_button(min_val, max_val, callback)
def register_adc_button_push(self, pin, min_val, max_val, pullup, callback):
def helper(eventtime, state, callback=callback):
if state:
callback(eventtime)
self.register_adc_button(pin, min_val, max_val, pullup, helper)
def register_buttons(self, pins, callback):
# Parse pins
ppins = self.printer.lookup_object('pins')
mcu = mcu_name = None
pin_params_list = []
for pin in pins:
pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True)
if mcu is not None and pin_params['chip'] != mcu:
raise ppins.error("button pins must be on same mcu")
mcu = pin_params['chip']
mcu_name = pin_params['chip_name']
pin_params_list.append(pin_params)
# Register pins and callback with the appropriate MCU
mcu_buttons = self.mcu_buttons.get(mcu_name)
if (mcu_buttons is None
or len(mcu_buttons.pin_list) + len(pin_params_list) > 8):
self.mcu_buttons[mcu_name] = mcu_buttons = MCU_buttons(
self.printer, mcu)
mcu_buttons.setup_buttons(pin_params_list, callback)
def register_rotary_encoder(self, pin1, pin2, cw_callback, ccw_callback,
steps_per_detent):
if steps_per_detent == 2:
re = HalfStepRotaryEncoder(cw_callback, ccw_callback)
elif steps_per_detent == 4:
re = FullStepRotaryEncoder(cw_callback, ccw_callback)
else:
raise self.printer.config_error(
"%d steps per detent not supported" % steps_per_detent)
self.register_buttons([pin1, pin2], re.encoder_callback)
def register_button_push(self, pin, callback):
def helper(eventtime, state, callback=callback):
if state:
callback(eventtime)
self.register_buttons([pin], helper)
def load_config(config):
return PrinterButtons(config)
+70
View File
@@ -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)
+125
View File
@@ -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)
+54
View File
@@ -0,0 +1,54 @@
# A simple timer for executing gcode templates
#
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
class DelayedGcode:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.name = config.get_name().split()[1]
self.gcode = self.printer.lookup_object('gcode')
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.timer_gcode = gcode_macro.load_template(config, 'gcode')
self.duration = config.getfloat('initial_duration', 0., minval=0.)
self.timer_handler = None
self.inside_timer = self.repeat = False
self.printer.register_event_handler("klippy:ready", self._handle_ready)
self.gcode.register_mux_command(
"UPDATE_DELAYED_GCODE", "ID", self.name,
self.cmd_UPDATE_DELAYED_GCODE,
desc=self.cmd_UPDATE_DELAYED_GCODE_help)
def _handle_ready(self):
waketime = self.reactor.NEVER
if self.duration:
waketime = self.reactor.monotonic() + self.duration
self.timer_handler = self.reactor.register_timer(
self._gcode_timer_event, waketime)
def _gcode_timer_event(self, eventtime):
self.inside_timer = True
try:
self.gcode.run_script(self.timer_gcode.render())
except Exception:
logging.exception("Script running error")
nextwake = self.reactor.NEVER
if self.repeat:
nextwake = eventtime + self.duration
self.inside_timer = self.repeat = False
return nextwake
cmd_UPDATE_DELAYED_GCODE_help = "Update the duration of a delayed_gcode"
def cmd_UPDATE_DELAYED_GCODE(self, gcmd):
self.duration = gcmd.get_float('DURATION', minval=0.)
if self.inside_timer:
self.repeat = (self.duration != 0.)
else:
waketime = self.reactor.NEVER
if self.duration:
waketime = self.reactor.monotonic() + self.duration
self.reactor.update_timer(self.timer_handler, waketime)
def load_config_prefix(config):
return DelayedGcode(config)
+287
View File
@@ -0,0 +1,287 @@
# Delta calibration support
#
# Copyright (C) 2017-2019 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging, collections
import mathutil
from . import probe
# A "stable position" is a 3-tuple containing the number of steps
# taken since hitting the endstop on each delta tower. Delta
# calibration uses this coordinate system because it allows a position
# to be described independent of the software parameters.
# Load a stable position from a config entry
def load_config_stable(config, option):
return config.getfloatlist(option, count=3)
######################################################################
# Delta calibration object
######################################################################
# The angles and distances of the calibration object found in
# docs/prints/calibrate_size.stl
MeasureAngles = [210., 270., 330., 30., 90., 150.]
MeasureOuterRadius = 65
MeasureRidgeRadius = 5. - .5
# How much to prefer a distance measurement over a height measurement
MEASURE_WEIGHT = 0.5
# Convert distance measurements made on the calibration object to
# 3-tuples of (actual_distance, stable_position1, stable_position2)
def measurements_to_distances(measured_params, delta_params):
# Extract params
mp = measured_params
dp = delta_params
scale = mp['SCALE'][0]
cpw = mp['CENTER_PILLAR_WIDTHS']
center_widths = [cpw[0], cpw[2], cpw[1], cpw[0], cpw[2], cpw[1]]
center_dists = [od - cw
for od, cw in zip(mp['CENTER_DISTS'], center_widths)]
outer_dists = [
od - opw
for od, opw in zip(mp['OUTER_DISTS'], mp['OUTER_PILLAR_WIDTHS']) ]
# Convert angles in degrees to an XY multiplier
obj_angles = list(map(math.radians, MeasureAngles))
xy_angles = list(zip(map(math.cos, obj_angles), map(math.sin, obj_angles)))
# Calculate stable positions for center measurements
inner_ridge = MeasureRidgeRadius * scale
inner_pos = [(ax * inner_ridge, ay * inner_ridge, 0.)
for ax, ay in xy_angles]
outer_ridge = (MeasureOuterRadius + MeasureRidgeRadius) * scale
outer_pos = [(ax * outer_ridge, ay * outer_ridge, 0.)
for ax, ay in xy_angles]
center_positions = [
(cd, dp.calc_stable_position(ip), dp.calc_stable_position(op))
for cd, ip, op in zip(center_dists, inner_pos, outer_pos)]
# Calculate positions of outer measurements
outer_center = MeasureOuterRadius * scale
start_pos = [(ax * outer_center, ay * outer_center) for ax, ay in xy_angles]
shifted_angles = xy_angles[2:] + xy_angles[:2]
first_pos = [(ax * inner_ridge + spx, ay * inner_ridge + spy, 0.)
for (ax, ay), (spx, spy) in zip(shifted_angles, start_pos)]
second_pos = [(ax * outer_ridge + spx, ay * outer_ridge + spy, 0.)
for (ax, ay), (spx, spy) in zip(shifted_angles, start_pos)]
outer_positions = [
(od, dp.calc_stable_position(fp), dp.calc_stable_position(sp))
for od, fp, sp in zip(outer_dists, first_pos, second_pos)]
return center_positions + outer_positions
######################################################################
# Delta Calibrate class
######################################################################
class DeltaCalibrate:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
# Calculate default probing points
radius = config.getfloat('radius', above=0.)
points = [(0., 0.)]
scatter = [.95, .90, .85, .70, .75, .80]
for i in range(6):
r = math.radians(90. + 60. * i)
dist = radius * scatter[i]
points.append((math.cos(r) * dist, math.sin(r) * dist))
self.probe_helper = probe.ProbePointsHelper(
config, self.probe_finalize, default_points=points)
self.probe_helper.minimum_points(3)
# Restore probe stable positions
self.last_probe_positions = []
for i in range(999):
height = config.getfloat("height%d" % (i,), None)
if height is None:
break
height_pos = load_config_stable(config, "height%d_pos" % (i,))
self.last_probe_positions.append((height, height_pos))
# Restore manually entered heights
self.manual_heights = []
for i in range(999):
height = config.getfloat("manual_height%d" % (i,), None)
if height is None:
break
height_pos = load_config_stable(config, "manual_height%d_pos"
% (i,))
self.manual_heights.append((height, height_pos))
# Restore distance measurements
self.delta_analyze_entry = {'SCALE': (1.,)}
self.last_distances = []
for i in range(999):
dist = config.getfloat("distance%d" % (i,), None)
if dist is None:
break
distance_pos1 = load_config_stable(config, "distance%d_pos1" % (i,))
distance_pos2 = load_config_stable(config, "distance%d_pos2" % (i,))
self.last_distances.append((dist, distance_pos1, distance_pos2))
# Register gcode commands
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command('DELTA_CALIBRATE', self.cmd_DELTA_CALIBRATE,
desc=self.cmd_DELTA_CALIBRATE_help)
self.gcode.register_command('DELTA_ANALYZE', self.cmd_DELTA_ANALYZE,
desc=self.cmd_DELTA_ANALYZE_help)
def handle_connect(self):
kin = self.printer.lookup_object('toolhead').get_kinematics()
if not hasattr(kin, "get_calibration"):
raise self.printer.config_error(
"Delta calibrate is only for delta printers")
def save_state(self, probe_positions, distances, delta_params):
# Save main delta parameters
configfile = self.printer.lookup_object('configfile')
delta_params.save_state(configfile)
# Save probe stable positions
section = 'delta_calibrate'
configfile.remove_section(section)
for i, (z_offset, spos) in enumerate(probe_positions):
configfile.set(section, "height%d" % (i,), z_offset)
configfile.set(section, "height%d_pos" % (i,),
"%.3f,%.3f,%.3f" % tuple(spos))
# Save manually entered heights
for i, (z_offset, spos) in enumerate(self.manual_heights):
configfile.set(section, "manual_height%d" % (i,), z_offset)
configfile.set(section, "manual_height%d_pos" % (i,),
"%.3f,%.3f,%.3f" % tuple(spos))
# Save distance measurements
for i, (dist, spos1, spos2) in enumerate(distances):
configfile.set(section, "distance%d" % (i,), dist)
configfile.set(section, "distance%d_pos1" % (i,),
"%.3f,%.3f,%.3f" % tuple(spos1))
configfile.set(section, "distance%d_pos2" % (i,),
"%.3f,%.3f,%.3f" % tuple(spos2))
def probe_finalize(self, offsets, positions):
# Convert positions into (z_offset, stable_position) pairs
z_offset = offsets[2]
kin = self.printer.lookup_object('toolhead').get_kinematics()
delta_params = kin.get_calibration()
probe_positions = [(z_offset, delta_params.calc_stable_position(p))
for p in positions]
# Perform analysis
self.calculate_params(probe_positions, self.last_distances)
def calculate_params(self, probe_positions, distances):
height_positions = self.manual_heights + probe_positions
# Setup for coordinate descent analysis
kin = self.printer.lookup_object('toolhead').get_kinematics()
orig_delta_params = odp = kin.get_calibration()
adj_params, params = odp.coordinate_descent_params(distances)
logging.info("Calculating delta_calibrate with:\n%s\n%s\n"
"Initial delta_calibrate parameters: %s",
height_positions, distances, params)
z_weight = 1.
if distances:
z_weight = len(distances) / (MEASURE_WEIGHT * len(probe_positions))
# Perform coordinate descent
def delta_errorfunc(params):
try:
# Build new delta_params for params under test
delta_params = orig_delta_params.new_calibration(params)
getpos = delta_params.get_position_from_stable
# Calculate z height errors
total_error = 0.
for z_offset, stable_pos in height_positions:
x, y, z = getpos(stable_pos)
total_error += (z - z_offset)**2
total_error *= z_weight
# Calculate distance errors
for dist, stable_pos1, stable_pos2 in distances:
x1, y1, z1 = getpos(stable_pos1)
x2, y2, z2 = getpos(stable_pos2)
d = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
total_error += (d - dist)**2
return total_error
except ValueError:
return 9999999999999.9
new_params = mathutil.background_coordinate_descent(
self.printer, adj_params, params, delta_errorfunc)
# Log and report results
logging.info("Calculated delta_calibrate parameters: %s", new_params)
new_delta_params = orig_delta_params.new_calibration(new_params)
for z_offset, spos in height_positions:
logging.info("height orig: %.6f new: %.6f goal: %.6f",
orig_delta_params.get_position_from_stable(spos)[2],
new_delta_params.get_position_from_stable(spos)[2],
z_offset)
for dist, spos1, spos2 in distances:
x1, y1, z1 = orig_delta_params.get_position_from_stable(spos1)
x2, y2, z2 = orig_delta_params.get_position_from_stable(spos2)
orig_dist = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
x1, y1, z1 = new_delta_params.get_position_from_stable(spos1)
x2, y2, z2 = new_delta_params.get_position_from_stable(spos2)
new_dist = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
logging.info("distance orig: %.6f new: %.6f goal: %.6f",
orig_dist, new_dist, dist)
# Store results for SAVE_CONFIG
self.save_state(probe_positions, distances, new_delta_params)
self.gcode.respond_info(
"The SAVE_CONFIG command will update the printer config file\n"
"with these parameters and restart the printer.")
cmd_DELTA_CALIBRATE_help = "Delta calibration script"
def cmd_DELTA_CALIBRATE(self, gcmd):
self.probe_helper.start_probe(gcmd)
def add_manual_height(self, height):
# Determine current location of toolhead
toolhead = self.printer.lookup_object('toolhead')
toolhead.flush_step_generation()
kin = toolhead.get_kinematics()
kin_spos = {s.get_name(): s.get_commanded_position()
for s in kin.get_steppers()}
kin_pos = kin.calc_position(kin_spos)
# Convert location to a stable position
delta_params = kin.get_calibration()
stable_pos = tuple(delta_params.calc_stable_position(kin_pos))
# Add to list of manual heights
self.manual_heights.append((height, stable_pos))
self.gcode.respond_info(
"Adding manual height: %.3f,%.3f,%.3f is actually z=%.3f"
% (kin_pos[0], kin_pos[1], kin_pos[2], height))
def do_extended_calibration(self):
# Extract distance positions
if len(self.delta_analyze_entry) <= 1:
distances = self.last_distances
elif len(self.delta_analyze_entry) < 5:
raise self.gcode.error("Not all measurements provided")
else:
kin = self.printer.lookup_object('toolhead').get_kinematics()
delta_params = kin.get_calibration()
distances = measurements_to_distances(
self.delta_analyze_entry, delta_params)
if not self.last_probe_positions:
raise self.gcode.error(
"Must run basic calibration with DELTA_CALIBRATE first")
# Perform analysis
self.calculate_params(self.last_probe_positions, distances)
cmd_DELTA_ANALYZE_help = "Extended delta calibration tool"
def cmd_DELTA_ANALYZE(self, gcmd):
# Check for manual height entry
mheight = gcmd.get_float('MANUAL_HEIGHT', None)
if mheight is not None:
self.add_manual_height(mheight)
return
# Parse distance measurements
args = {'CENTER_DISTS': 6, 'CENTER_PILLAR_WIDTHS': 3,
'OUTER_DISTS': 6, 'OUTER_PILLAR_WIDTHS': 6, 'SCALE': 1}
for name, count in args.items():
data = gcmd.get(name, None)
if data is None:
continue
try:
parts = list(map(float, data.split(',')))
except:
raise gcmd.error("Unable to parse parameter '%s'" % (name,))
if len(parts) != count:
raise gcmd.error("Parameter '%s' must have %d values"
% (name, count))
self.delta_analyze_entry[name] = parts
logging.info("DELTA_ANALYZE %s = %s", name, parts)
# Perform analysis if requested
action = gcmd.get('CALIBRATE', None)
if action is not None:
if action != 'extended':
raise gcmd.error("Unknown calibrate action")
self.do_extended_calibration()
def load_config(config):
return DeltaCalibrate(config)
+111
View File
@@ -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)
+19
View File
@@ -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)
+461
View File
@@ -0,0 +1,461 @@
# This file defines the default layout of the printer's lcd display.
# It is not necessary to edit this file to change the display.
# Instead, one may override any of the sections defined here by
# defining a section with the same name in the main printer.cfg config
# file.
######################################################################
# Helper macros for showing common screen values
######################################################################
[display_template _heater_temperature]
param_heater_name: "extruder"
text:
{% if param_heater_name in printer %}
{% set heater = printer[param_heater_name] %}
# Show glyph
{% if param_heater_name == "heater_bed" %}
{% if heater.target %}
{% set frame = (printer.toolhead.estimated_print_time|int % 2) + 1 %}
~bed_heat{frame}~
{% else %}
~bed~
{% endif %}
{% else %}
~extruder~
{% endif %}
# Show temperature
{ "%3.0f" % (heater.temperature,) }
# Optionally show target
{% if heater.target and (heater.temperature - heater.target)|abs > 2 %}
~right_arrow~
{ "%0.0f" % (heater.target,) }
{% endif %}
~degrees~
{% endif %}
[display_template _fan_speed]
text:
{% if 'fan' in printer %}
{% set speed = printer.fan.speed %}
{% if speed %}
{% set frame = (printer.toolhead.estimated_print_time|int % 2) + 1 %}
~fan{frame}~
{% else %}
~fan1~
{% endif %}
{ "{:>4.0%}".format(speed) }
{% endif %}
[display_template _printing_time]
text:
{% set ptime = printer.idle_timeout.printing_time %}
{ "%02d:%02d" % (ptime // (60 * 60), (ptime // 60) % 60) }
[display_template _print_status]
text:
{% if printer.display_status.message %}
{ printer.display_status.message }
{% elif printer.idle_timeout.printing_time %}
{% set pos = printer.toolhead.position %}
{ "X%-4.0fY%-4.0fZ%-5.2f" % (pos.x, pos.y, pos.z) }
{% else %}
Ready
{% endif %}
######################################################################
# Default 16x4 display
######################################################################
[display_data _default_16x4 extruder]
position: 0, 0
text:
{% set active_extruder = printer.toolhead.extruder %}
{ render("_heater_temperature", param_heater_name=active_extruder) }
[display_data _default_16x4 fan]
position: 0, 10
text: { render("_fan_speed") }
[display_data _default_16x4 heater_bed]
position: 1, 0
text: { render("_heater_temperature", param_heater_name="heater_bed") }
[display_data _default_16x4 speed_factor]
position: 1, 10
text:
~feedrate~
{ "{:>4.0%}".format(printer.gcode_move.speed_factor) }
[display_data _default_16x4 print_progress]
position: 2, 0
text: { "{:^10.0%}".format(printer.display_status.progress) }
[display_data _default_16x4 progress_bar]
position: 2, 1 # Draw graphical progress bar after text is written
text: { draw_progress_bar(2, 0, 10, printer.display_status.progress) }
[display_data _default_16x4 printing_time]
position: 2, 10
text: { "%6s" % (render("_printing_time").strip(),) }
[display_data _default_16x4 print_status]
position: 3, 0
text: { render("_print_status") }
######################################################################
# Alternative 16x4 layout for multi-extruders
######################################################################
[display_data _multiextruder_16x4 extruder]
position: 0, 0
text: { render("_heater_temperature", param_heater_name="extruder") }
[display_data _multiextruder_16x4 fan]
position: 0, 10
text: { render("_fan_speed") }
[display_data _multiextruder_16x4 extruder1]
position: 1, 0
text: { render("_heater_temperature", param_heater_name="extruder1") }
[display_data _multiextruder_16x4 print_progress]
position: 1, 10
text: { "{:^6.0%}".format(printer.display_status.progress) }
[display_data _multiextruder_16x4 progress_bar]
position: 1, 11 # Draw graphical progress bar after text is written
text: { draw_progress_bar(1, 10, 6, printer.display_status.progress) }
[display_data _multiextruder_16x4 heater_bed]
position: 2, 0
text: { render("_heater_temperature", param_heater_name="heater_bed") }
[display_data _multiextruder_16x4 printing_time]
position: 2, 10
text: { "%6s" % (render("_printing_time").strip(),) }
[display_data _multiextruder_16x4 print_status]
position: 3, 0
text: { render("_print_status") }
######################################################################
# Default 20x4 display
######################################################################
[display_data _default_20x4 extruder]
position: 0, 0
text: { render("_heater_temperature", param_heater_name="extruder") }
[display_data _default_20x4 heater_bed]
position: 0, 10
text: { render("_heater_temperature", param_heater_name="heater_bed") }
[display_data _default_20x4 extruder1]
position: 1, 0
text: { render("_heater_temperature", param_heater_name="extruder1") }
[display_data _default_20x4 fan]
position: 1, 10
text:
{% if 'fan' in printer %}
{ "Fan {:^4.0%}".format(printer.fan.speed) }
{% endif %}
[display_data _default_20x4 speed_factor]
position: 2, 0
text:
~feedrate~
{ "{:^4.0%}".format(printer.gcode_move.speed_factor) }
[display_data _default_20x4 print_progress]
position: 2, 8
text:
{% if 'virtual_sdcard' in printer and printer.virtual_sdcard.progress %}
~sd~
{% else %}
~usb~
{% endif %}
{ "{:^4.0%}".format(printer.display_status.progress) }
[display_data _default_20x4 printing_time]
position: 2, 14
text:
~clock~
{ render("_printing_time") }
[display_data _default_20x4 print_status]
position: 3, 0
text: { render("_print_status") }
######################################################################
# Default 16x4 glyphs
######################################################################
[display_glyph extruder]
data:
................
................
..************..
.....******.....
..************..
.....******.....
..************..
................
....********....
....******.*....
....********....
................
......****......
.......**.......
................
................
[display_glyph bed]
data:
................
................
................
................
................
................
................
................
................
................
................
...*********....
..*.........*...
.*************..
................
................
[display_glyph bed_heat1]
data:
................
................
..*....*....*...
.*....*....*....
..*....*....*...
...*....*....*..
..*....*....*...
.*....*....*....
..*....*....*...
................
................
...*********....
..*.........*...
.*************..
................
................
[display_glyph bed_heat2]
data:
................
................
..*....*....*...
...*....*....*..
..*....*....*...
.*....*....*....
..*....*....*...
...*....*....*..
..*....*....*...
................
................
...*********....
..*.........*...
.*************..
................
................
[display_glyph fan1]
data:
................
................
....***.........
...****....**...
...****...****..
....***..*****..
.....*....****..
.......**.......
.......**.......
..****....*.....
..*****..***....
..****...****...
...**....****...
.........***....
................
................
[display_glyph fan2]
data:
................
................
.......****.....
.......****.....
.......***......
..**...**.......
..***...........
..****.**.****..
..****.**.****..
...........***..
.......**...**..
......***.......
.....****.......
.....****.......
................
................
[display_glyph feedrate]
data:
................
................
***.***.***.**..
*...*...*...*.*.
**..**..**..*.*.
*...*...*...*.*.
*...***.***.**..
................
**...*..***.***.
*.*.*.*..*..*...
**..***..*..**..
*.*.*.*..*..*...
*.*.*.*..*..***.
................
................
................
# In addition to the above glyphs, 16x4 displays also have the
# following hard-coded single character glyphs: right_arrow, degrees.
######################################################################
# Default 20x4 glyphs
######################################################################
[display_glyph extruder]
hd44780_slot: 0
hd44780_data:
..*..
.*.*.
.*.*.
.*.*.
.*.*.
*...*
*...*
.***.
[display_glyph bed]
hd44780_slot: 1
hd44780_data:
.....
*****
*.*.*
*...*
*.*.*
*****
.....
.....
[display_glyph bed_heat1]
hd44780_slot: 1
hd44780_data:
.*..*
*..*.
.*..*
*..*.
.....
*****
.....
.....
[display_glyph bed_heat2]
hd44780_slot: 1
hd44780_data:
*..*.
.*..*
*..*.
.*..*
.....
*****
.....
.....
[display_glyph fan]
hd44780_slot: 2
hd44780_data:
.....
*..**
**.*.
..*..
.*.**
**..*
.....
.....
[display_glyph feedrate]
hd44780_slot: 3
hd44780_data:
***..
*....
**...
*.***
..*.*
..**.
..*.*
.....
[display_glyph clock]
hd44780_slot: 4
hd44780_data:
.....
.***.
*..**
*.*.*
*...*
.***.
.....
.....
[display_glyph degrees]
hd44780_slot: 5
hd44780_data:
.**..
*..*.
*..*.
.**..
.....
.....
.....
.....
[display_glyph usb]
hd44780_slot: 6
hd44780_data:
.***.
.***.
.***.
*****
*****
*****
..*..
..*..
[display_glyph sd]
hd44780_slot: 6
hd44780_data:
.....
..***
.****
*****
*****
*****
*****
.....
# In addition to the above glyphs, 20x4 displays also have the
# following hard-coded glyphs: right_arrow.
+275
View File
@@ -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)
+276
View File
@@ -0,0 +1,276 @@
# Fonts for connected displays
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
######################################################################
# Font - VGA 8x14, Row Major, MSB, 2 bytes padding
#
# Font comes from fntcol16.zip package found at:
# ftp://ftp.simtel.net/pub/simtelnet/msdos/screen/fntcol16.zip
# (c) Joseph Gil
#
# Indivdual fonts are public domain
######################################################################
VGA_FONT = [
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x7e\x81\xa5\x81\x81\xbd\x99\x81\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x7e\xff\xdb\xff\xff\xc3\xe7\xff\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x6c\xfe\xfe\xfe\xfe\x7c\x38\x10\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x10\x38\x7c\xfe\x7c\x38\x10\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x3c\xe7\xe7\xe7\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x7e\xff\xff\x7e\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x18\x3c\x3c\x18\x00\x00\x00\x00\x00\x00',
b'\x00\xff\xff\xff\xff\xff\xe7\xc3\xc3\xe7\xff\xff\xff\xff\xff\x00',
b'\x00\x00\x00\x00\x00\x3c\x66\x42\x42\x66\x3c\x00\x00\x00\x00\x00',
b'\x00\xff\xff\xff\xff\xc3\x99\xbd\xbd\x99\xc3\xff\xff\xff\xff\x00',
b'\x00\x00\x00\x1e\x0e\x1a\x32\x78\xcc\xcc\xcc\x78\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x66\x66\x66\x3c\x18\x7e\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x3f\x33\x3f\x30\x30\x30\x70\xf0\xe0\x00\x00\x00\x00',
b'\x00\x00\x00\x7f\x63\x7f\x63\x63\x63\x67\xe7\xe6\xc0\x00\x00\x00',
b'\x00\x00\x00\x18\x18\xdb\x3c\xe7\x3c\xdb\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x80\xc0\xe0\xf8\xfe\xf8\xe0\xc0\x80\x00\x00\x00\x00',
b'\x00\x00\x00\x02\x06\x0e\x3e\xfe\x3e\x0e\x06\x02\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x66\x66\x66\x66\x66\x66\x00\x66\x66\x00\x00\x00\x00',
b'\x00\x00\x00\x7f\xdb\xdb\xdb\x7b\x1b\x1b\x1b\x1b\x00\x00\x00\x00',
b'\x00\x00\x7c\xc6\x60\x38\x6c\xc6\xc6\x6c\x38\x0c\xc6\x7c\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xfe\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x7e\x18\x18\x18\x7e\x3c\x18\x7e\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x7e\x18\x18\x18\x18\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x18\x18\x18\x18\x18\x7e\x3c\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x18\x0c\xfe\x0c\x18\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x30\x60\xfe\x60\x30\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xc0\xc0\xc0\xfe\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x28\x6c\xfe\x6c\x28\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x10\x38\x38\x7c\x7c\xfe\xfe\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\xfe\xfe\x7c\x7c\x38\x38\x10\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x3c\x3c\x3c\x18\x18\x00\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x66\x66\x66\x24\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x6c\x6c\xfe\x6c\x6c\x6c\xfe\x6c\x6c\x00\x00\x00\x00',
b'\x00\x18\x18\x7c\xc6\xc2\xc0\x7c\x06\x86\xc6\x7c\x18\x18\x00\x00',
b'\x00\x00\x00\x00\x00\xc2\xc6\x0c\x18\x30\x66\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\x6c\x38\x76\xdc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x30\x30\x30\x60\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x0c\x18\x30\x30\x30\x30\x30\x18\x0c\x00\x00\x00\x00',
b'\x00\x00\x00\x30\x18\x0c\x0c\x0c\x0c\x0c\x18\x30\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x66\x3c\xff\x3c\x66\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x18\x30\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x02\x06\x0c\x18\x30\x60\xc0\x80\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xce\xde\xf6\xe6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x38\x78\x18\x18\x18\x18\x18\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\x06\x0c\x18\x30\x60\xc6\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\x06\x06\x3c\x06\x06\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x0c\x1c\x3c\x6c\xcc\xfe\x0c\x0c\x1e\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\xc0\xc0\xc0\xfc\x06\x06\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x60\xc0\xc0\xfc\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\xc6\x06\x0c\x18\x30\x30\x30\x30\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\xc6\x7c\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\xc6\x7e\x06\x06\x0c\x78\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x18\x18\x00\x00\x00\x18\x18\x30\x00\x00\x00\x00',
b'\x00\x00\x00\x06\x0c\x18\x30\x60\x30\x18\x0c\x06\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7e\x00\x00\x7e\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x60\x30\x18\x0c\x06\x0c\x18\x30\x60\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\x0c\x18\x18\x00\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\xde\xde\xde\xdc\xc0\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\xfc\x66\x66\x66\x7c\x66\x66\x66\xfc\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x66\xc2\xc0\xc0\xc0\xc2\x66\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\xf8\x6c\x66\x66\x66\x66\x66\x6c\xf8\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\x66\x62\x68\x78\x68\x62\x66\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\x66\x62\x68\x78\x68\x60\x60\xf0\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x66\xc2\xc0\xc0\xde\xc6\x66\x3a\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\xc6\xc6\xfe\xc6\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x1e\x0c\x0c\x0c\x0c\x0c\xcc\xcc\x78\x00\x00\x00\x00',
b'\x00\x00\x00\xe6\x66\x6c\x6c\x78\x6c\x6c\x66\xe6\x00\x00\x00\x00',
b'\x00\x00\x00\xf0\x60\x60\x60\x60\x60\x62\x66\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xee\xfe\xfe\xd6\xc6\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\xc6\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00\x00',
b'\x00\x00\x00\xfc\x66\x66\x66\x7c\x60\x60\x60\xf0\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xd6\xde\x7c\x0c\x0e\x00\x00\x00',
b'\x00\x00\x00\xfc\x66\x66\x66\x7c\x6c\x66\x66\xe6\x00\x00\x00\x00',
b'\x00\x00\x00\x7c\xc6\xc6\x60\x38\x0c\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x7e\x7e\x5a\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\xc6\xc6\xc6\xc6\x6c\x38\x10\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\xc6\xc6\xd6\xd6\xfe\x7c\x6c\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\x6c\x38\x38\x38\x6c\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x66\x66\x66\x66\x3c\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\xc6\x8c\x18\x30\x60\xc2\xc6\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x30\x30\x30\x30\x30\x30\x30\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x80\xc0\xe0\x70\x38\x1c\x0e\x06\x02\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x3c\x00\x00\x00\x00',
b'\x00\x10\x38\x6c\xc6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00',
b'\x00\x30\x30\x18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\xe0\x60\x60\x78\x6c\x66\x66\x66\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7c\xc6\xc0\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x1c\x0c\x0c\x3c\x6c\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\xf0\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\x7c\x0c\xcc\x78\x00\x00',
b'\x00\x00\x00\xe0\x60\x60\x6c\x76\x66\x66\x66\xe6\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x18\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x06\x06\x00\x0e\x06\x06\x06\x06\x66\x66\x3c\x00\x00',
b'\x00\x00\x00\xe0\x60\x60\x66\x6c\x78\x6c\x66\xe6\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x18\x18\x18\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xec\xfe\xd6\xd6\xd6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x66\x66\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xdc\x66\x66\x66\x7c\x60\x60\xf0\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x76\xcc\xcc\xcc\x7c\x0c\x0c\x1e\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xdc\x76\x66\x60\x60\xf0\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7c\xc6\x70\x1c\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x10\x30\x30\xfc\x30\x30\x30\x36\x1c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x66\x66\x66\x66\x3c\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xc6\xc6\xd6\xd6\xfe\x6c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xc6\x6c\x38\x38\x6c\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xc6\xc6\xc6\xc6\x7e\x06\x0c\xf8\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xfe\xcc\x18\x30\x66\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x0e\x18\x18\x18\x70\x18\x18\x18\x0e\x00\x00\x00\x00',
b'\x00\x00\x00\x18\x18\x18\x18\x00\x18\x18\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x70\x18\x18\x18\x0e\x18\x18\x18\x70\x00\x00\x00\x00',
b'\x00\x00\x00\x76\xdc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x10\x38\x6c\xc6\xc6\xfe\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x3c\x66\xc2\xc0\xc0\xc2\x66\x3c\x0c\x06\x7c\x00\x00',
b'\x00\x00\x00\xcc\xcc\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x0c\x18\x30\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x10\x38\x6c\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\xcc\xcc\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x38\x6c\x38\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x3c\x66\x60\x66\x3c\x0c\x06\x3c\x00\x00\x00',
b'\x00\x00\x10\x38\x6c\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\xcc\xcc\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\x7c\xc6\xfe\xc0\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x66\x66\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x18\x3c\x66\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\xc6\xc6\x10\x38\x6c\xc6\xc6\xfe\xc6\xc6\x00\x00\x00\x00',
b'\x00\x38\x6c\x38\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\x00\x00\x00\x00',
b'\x00\x18\x30\x60\x00\xfe\x66\x60\x7c\x60\x66\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\xcc\x76\x36\x7e\xd8\xd8\x6e\x00\x00\x00\x00',
b'\x00\x00\x00\x3e\x6c\xcc\xcc\xfe\xcc\xcc\xcc\xce\x00\x00\x00\x00',
b'\x00\x00\x10\x38\x6c\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x30\x78\xcc\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x60\x30\x18\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\xc6\xc6\x00\xc6\xc6\xc6\xc6\x7e\x06\x0c\x78\x00\x00',
b'\x00\x00\xc6\xc6\x38\x6c\xc6\xc6\xc6\xc6\x6c\x38\x00\x00\x00\x00',
b'\x00\x00\xc6\xc6\x00\xc6\xc6\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x18\x18\x3c\x66\x60\x60\x66\x3c\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x38\x6c\x64\x60\xf0\x60\x60\x60\xe6\xfc\x00\x00\x00\x00',
b'\x00\x00\x00\x66\x66\x3c\x18\x7e\x18\x7e\x18\x18\x00\x00\x00\x00',
b'\x00\x00\xf8\xcc\xcc\xf8\xc4\xcc\xde\xcc\xcc\xc6\x00\x00\x00\x00',
b'\x00\x00\x0e\x1b\x18\x18\x18\x7e\x18\x18\x18\x18\xd8\x70\x00\x00',
b'\x00\x00\x18\x30\x60\x00\x78\x0c\x7c\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x0c\x18\x30\x00\x38\x18\x18\x18\x18\x3c\x00\x00\x00\x00',
b'\x00\x00\x18\x30\x60\x00\x7c\xc6\xc6\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x18\x30\x60\x00\xcc\xcc\xcc\xcc\xcc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x76\xdc\x00\xdc\x66\x66\x66\x66\x66\x00\x00\x00\x00',
b'\x00\x76\xdc\x00\xc6\xe6\xf6\xfe\xde\xce\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x3c\x6c\x6c\x3e\x00\x7e\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x38\x6c\x6c\x38\x00\x7c\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x30\x30\x00\x30\x30\x60\xc6\xc6\x7c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\xfe\xc0\xc0\xc0\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\xfe\x06\x06\x06\x00\x00\x00\x00\x00',
b'\x00\x00\xc0\xc0\xc6\xcc\xd8\x30\x60\xdc\x86\x0c\x18\x3e\x00\x00',
b'\x00\x00\xc0\xc0\xc6\xcc\xd8\x30\x66\xce\x9e\x3e\x06\x06\x00\x00',
b'\x00\x00\x00\x18\x18\x00\x18\x18\x3c\x3c\x3c\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x36\x6c\xd8\x6c\x36\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\xd8\x6c\x36\x6c\xd8\x00\x00\x00\x00\x00\x00',
b'\x00\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x11\x44\x00',
b'\x00\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x55\xaa\x00',
b'\x00\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\xdd\x77\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\xf8\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\xf8\x18\xf8\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\xf6\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xfe\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x00\x00\x00\x00\x00\xf8\x18\xf8\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x36\x36\x36\x36\x36\xf6\x06\xf6\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x00\x00\x00\x00\x00\xfe\x06\xf6\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\xf6\x06\xfe\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\xfe\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x18\x18\x18\x18\x18\xf8\x18\xf8\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\x1f\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xff\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\x1f\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\xff\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x1f\x18\x1f\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\x37\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x37\x30\x3f\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x3f\x30\x37\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\xf7\x00\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xff\x00\xf7\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x37\x30\x37\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x00\x00\x00\x00\x00\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x36\x36\x36\x36\x36\xf7\x00\xf7\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x18\x18\x18\x18\x18\xff\x00\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\xff\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\xff\x00\xff\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xff\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\x3f\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x18\x18\x18\x18\x18\x1f\x18\x1f\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x1f\x18\x1f\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x3f\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x36\x36\x36\x36\x36\x36\x36\xff\x36\x36\x36\x36\x36\x36\x00',
b'\x00\x18\x18\x18\x18\x18\xff\x18\xff\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\xf8\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x18\x18\x18\x18\x18\x18\x00',
b'\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\x00',
b'\x00\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\xf0\x00',
b'\x00\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x0f\x00',
b'\x00\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x76\xdc\xd8\xd8\xdc\x76\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x7c\xc6\xfc\xc6\xc6\xfc\xc0\xc0\x40\x00\x00',
b'\x00\x00\x00\xfe\xc6\xc6\xc0\xc0\xc0\xc0\xc0\xc0\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\xfe\x6c\x6c\x6c\x6c\x6c\x6c\x00\x00\x00\x00',
b'\x00\x00\x00\xfe\xc6\x60\x30\x18\x30\x60\xc6\xfe\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7e\xd8\xd8\xd8\xd8\x70\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x66\x66\x66\x66\x7c\x60\x60\xc0\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x76\xdc\x18\x18\x18\x18\x18\x00\x00\x00\x00',
b'\x00\x00\x00\x7e\x18\x3c\x66\x66\x66\x3c\x18\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\xc6\xc6\xfe\xc6\xc6\x6c\x38\x00\x00\x00\x00',
b'\x00\x00\x00\x38\x6c\xc6\xc6\xc6\x6c\x6c\x6c\xee\x00\x00\x00\x00',
b'\x00\x00\x00\x1e\x30\x18\x0c\x3e\x66\x66\x66\x3c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x7e\xdb\xdb\x7e\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x03\x06\x7e\xdb\xdb\xf3\x7e\x60\xc0\x00\x00\x00\x00',
b'\x00\x00\x00\x1c\x30\x60\x60\x7c\x60\x60\x30\x1c\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x7c\xc6\xc6\xc6\xc6\xc6\xc6\xc6\x00\x00\x00\x00',
b'\x00\x00\x00\x00\xfe\x00\x00\xfe\x00\x00\xfe\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x18\x18\x7e\x18\x18\x00\x00\xff\x00\x00\x00\x00',
b'\x00\x00\x00\x30\x18\x0c\x06\x0c\x18\x30\x00\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x0c\x18\x30\x60\x30\x18\x0c\x00\x7e\x00\x00\x00\x00',
b'\x00\x00\x00\x0e\x1b\x1b\x18\x18\x18\x18\x18\x18\x18\x18\x18\x00',
b'\x00\x18\x18\x18\x18\x18\x18\x18\x18\xd8\xd8\x70\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x18\x18\x00\x7e\x00\x18\x18\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x76\xdc\x00\x76\xdc\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x38\x6c\x6c\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x18\x18\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x0f\x0c\x0c\x0c\x0c\x0c\xec\x6c\x3c\x1c\x00\x00\x00\x00',
b'\x00\x00\xd8\x6c\x6c\x6c\x6c\x6c\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x70\xd8\x30\x60\xc8\xf8\x00\x00\x00\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x7c\x7c\x7c\x7c\x7c\x7c\x00\x00\x00\x00\x00',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
]
+134
View File
@@ -0,0 +1,134 @@
# Support for HD44780 (20x4 text) LCD displays
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
LINE_LENGTH_DEFAULT=20
LINE_LENGTH_OPTIONS={16:16, 20:20}
TextGlyphs = { 'right_arrow': b'\x7e' }
HD44780_DELAY = .000040
class HD44780:
def __init__(self, config):
self.printer = config.get_printer()
# pin config
ppins = self.printer.lookup_object('pins')
pins = [ppins.lookup_pin(config.get(name + '_pin'))
for name in ['rs', 'e', 'd4', 'd5', 'd6', 'd7']]
self.hd44780_protocol_init = config.getboolean('hd44780_protocol_init',
True)
self.line_length = config.getchoice('line_length', LINE_LENGTH_OPTIONS,
LINE_LENGTH_DEFAULT)
mcu = None
for pin_params in pins:
if mcu is not None and pin_params['chip'] != mcu:
raise ppins.error("""{"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)
+125
View File
@@ -0,0 +1,125 @@
# Support for HD44780 (20x4 text) LCD displays
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
# Copyright (C) 2021 Marc-Andre Denis <marcadenis@msn.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from .. import bus
LINE_LENGTH_DEFAULT=20
LINE_LENGTH_OPTIONS={16:16, 20:20}
TextGlyphs = { 'right_arrow': b'\x7e' }
class hd44780_spi:
def __init__(self, config):
self.printer = config.get_printer()
self.hd44780_protocol_init = config.getboolean('hd44780_protocol_init',
True)
# spi config
self.spi = bus.MCU_SPI_from_config(
config, 0x00, pin_option="latch_pin")
self.mcu = self.spi.get_mcu()
#self.spi.spi_send([0x01,0xa0])
self.data_mask = (1<<1)
self.command_mask = 0
self.enable_mask = (1<<3)
self.icons = {}
self.line_length = config.getchoice('line_length', LINE_LENGTH_OPTIONS,
LINE_LENGTH_DEFAULT)
# framebuffers
self.text_framebuffers = [bytearray(b' '*2*self.line_length),
bytearray(b' '*2*self.line_length)]
self.glyph_framebuffer = bytearray(64)
self.all_framebuffers = [
# Text framebuffers
(self.text_framebuffers[0], bytearray(b'~'*2*self.line_length),
0x80),
(self.text_framebuffers[1], bytearray(b'~'*2*self.line_length),
0xc0),
# Glyph framebuffer
(self.glyph_framebuffer, bytearray(b'~'*64), 0x40) ]
def send_4_bits(self, cmd, is_data, minclock):
if is_data:
mask = self.data_mask
else:
mask = self.command_mask
self.spi.spi_send([(cmd & 0xF0) | mask], minclock)
self.spi.spi_send([(cmd & 0xF0) | mask | self.enable_mask], minclock)
self.spi.spi_send([(cmd & 0xF0) | mask], minclock)
def send(self, cmds, is_data=False, minclock=0):
for data in cmds:
self.send_4_bits(data,is_data,minclock)
self.send_4_bits(data<<4,is_data,minclock)
def flush(self):
# Find all differences in the framebuffers and send them to the chip
for new_data, old_data, fb_id in self.all_framebuffers:
if new_data == old_data:
continue
# Find the position of all changed bytes in this framebuffer
diffs = [[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data))
if n != o]
# Batch together changes that are close to each other
for i in range(len(diffs)-2, -1, -1):
pos, count = diffs[i]
nextpos, nextcount = diffs[i+1]
if pos + 4 >= nextpos and nextcount < 16:
diffs[i][1] = nextcount + (nextpos - pos)
del diffs[i+1]
# Transmit changes
for pos, count in diffs:
chip_pos = pos
self.send([fb_id + chip_pos])
self.send(new_data[pos:pos+count], is_data=True)
old_data[:] = new_data
def init(self):
curtime = self.printer.get_reactor().monotonic()
print_time = self.mcu.estimated_print_time(curtime)
# Program 4bit / 2-line mode and then issue 0x02 "Home" command
if self.hd44780_protocol_init:
init = [[0x33], [0x33], [0x32], [0x28, 0x28, 0x02]]
else:
init = [[0x02]]
# Reset (set positive direction ; enable display and hide cursor)
init.append([0x06, 0x0c])
for i, cmds in enumerate(init):
minclock = self.mcu.print_time_to_clock(print_time + i * .100)
self.send(cmds, minclock=minclock)
self.flush()
def write_text(self, x, y, data):
if x + len(data) > self.line_length:
data = data[:self.line_length - min(x, self.line_length)]
pos = x + ((y & 0x02) >> 1) * self.line_length
self.text_framebuffers[y & 1][pos:pos+len(data)] = data
def set_glyphs(self, glyphs):
for glyph_name, glyph_data in glyphs.items():
data = glyph_data.get('icon5x8')
if data is not None:
self.icons[glyph_name] = data
def write_glyph(self, x, y, glyph_name):
data = self.icons.get(glyph_name)
if data is not None:
slot, bits = data
self.write_text(x, y, [slot])
self.glyph_framebuffer[slot * 8:(slot + 1) * 8] = bits
return 1
char = TextGlyphs.get(glyph_name)
if char is not None:
# Draw character
self.write_text(x, y, char)
return 1
return 0
def write_graphics(self, x, y, data):
pass
def clear(self):
spaces = b' ' * 2*self.line_length
self.text_framebuffers[0][:] = spaces
self.text_framebuffers[1][:] = spaces
def get_dimensions(self):
return (self.line_length, 4)
+782
View File
@@ -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
+108
View File
@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
# Support for menu button press tracking
#
# Copyright (C) 2018 Janar Sööt <janar.soot@gmail.com>
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
LONG_PRESS_DURATION = 0.800
TIMER_DELAY = .200
class MenuKeys:
def __init__(self, config, callback):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.callback = callback
buttons = self.printer.load_object(config, "buttons")
# Register rotary encoder
encoder_pins = config.get('encoder_pins', None)
encoder_steps_per_detent = config.getchoice('encoder_steps_per_detent',
{2: 2, 4: 4}, 4)
if encoder_pins is not None:
try:
pin1, pin2 = encoder_pins.split(',')
except:
raise config.error("""{"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": []}""")
+257
View File
@@ -0,0 +1,257 @@
# Support for ST7920 (128x64 graphics) LCD displays
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from .. import bus
from . import font8x14
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
# Spec says 72us, but faster is possible in practice
ST7920_CMD_DELAY = .000020
ST7920_SYNC_DELAY = .000045
TextGlyphs = { 'right_arrow': b'\x1a' }
CharGlyphs = { 'degrees': bytearray(font8x14.VGA_FONT[0xf8]) }
class DisplayBase:
def __init__(self):
# framebuffers
self.text_framebuffer = bytearray(b' '*64)
self.glyph_framebuffer = bytearray(128)
self.graphics_framebuffers = [bytearray(32) for i in range(32)]
self.all_framebuffers = [
# Text framebuffer
(self.text_framebuffer, bytearray(b'~'*64), 0x80),
# Glyph framebuffer
(self.glyph_framebuffer, bytearray(b'~'*128), 0x40),
# Graphics framebuffers
] + [(self.graphics_framebuffers[i], bytearray(b'~'*32), i)
for i in range(32)]
self.cached_glyphs = {}
self.icons = {}
def flush(self):
# Find all differences in the framebuffers and send them to the chip
for new_data, old_data, fb_id in self.all_framebuffers:
if new_data == old_data:
continue
# Find the position of all changed bytes in this framebuffer
diffs = [[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data))
if n != o]
# Batch together changes that are close to each other
for i in range(len(diffs)-2, -1, -1):
pos, count = diffs[i]
nextpos, nextcount = diffs[i+1]
if pos + 5 >= nextpos and nextcount < 16:
diffs[i][1] = nextcount + (nextpos - pos)
del diffs[i+1]
# Transmit changes
for pos, count in diffs:
count += pos & 0x01
count += count & 0x01
pos = pos & ~0x01
chip_pos = pos >> 1
if fb_id < 0x40:
# Graphics framebuffer update
self.send([0x80 + fb_id, 0x80 + chip_pos], is_extended=True)
else:
self.send([fb_id + chip_pos])
self.send(new_data[pos:pos+count], is_data=True)
old_data[:] = new_data
def init(self):
cmds = [0x24, # Enter extended mode
0x40, # Clear vertical scroll address
0x02, # Enable CGRAM access
0x26, # Enable graphics
0x22, # Leave extended mode
0x02, # Home the display
0x06, # Set positive update direction
0x0c] # Enable display and hide cursor
self.send(cmds)
self.flush()
def cache_glyph(self, glyph_name, base_glyph_name, glyph_id):
icon = self.icons.get(glyph_name)
base_icon = self.icons.get(base_glyph_name)
if icon is None or base_icon is None:
return
all_bits = zip(icon[0], icon[1], base_icon[0], base_icon[1])
for i, (ic1, ic2, b1, b2) in enumerate(all_bits):
x1, x2 = ic1 ^ b1, ic2 ^ b2
pos = glyph_id*32 + i*2
self.glyph_framebuffer[pos:pos+2] = [x1, x2]
self.all_framebuffers[1][1][pos:pos+2] = [x1 ^ 1, x2 ^ 1]
self.cached_glyphs[glyph_name] = (base_glyph_name, (0, glyph_id*2))
def set_glyphs(self, glyphs):
for glyph_name, glyph_data in glyphs.items():
icon = glyph_data.get('icon16x16')
if icon is not None:
self.icons[glyph_name] = icon
# Setup animated glyphs
self.cache_glyph('fan2', 'fan1', 0)
self.cache_glyph('bed_heat2', 'bed_heat1', 1)
def write_text(self, x, y, data):
if x + len(data) > 16:
data = data[:16 - min(x, 16)]
pos = [0, 32, 16, 48][y] + x
self.text_framebuffer[pos:pos+len(data)] = data
def write_graphics(self, x, y, data):
if x >= 16 or y >= 4 or len(data) != 16:
return
gfx_fb = y * 16
if gfx_fb >= 32:
gfx_fb -= 32
x += 16
for i, bits in enumerate(data):
self.graphics_framebuffers[gfx_fb + i][x] = bits
def write_glyph(self, x, y, glyph_name):
glyph_id = self.cached_glyphs.get(glyph_name)
if glyph_id is not None and x & 1 == 0:
# Render cached icon using character generator
glyph_name = glyph_id[0]
self.write_text(x, y, glyph_id[1])
icon = self.icons.get(glyph_name)
if icon is not None:
# Draw icon in graphics mode
self.write_graphics(x, y, icon[0])
self.write_graphics(x + 1, y, icon[1])
return 2
char = TextGlyphs.get(glyph_name)
if char is not None:
# Draw character
self.write_text(x, y, char)
return 1
font = CharGlyphs.get(glyph_name)
if font is not None:
# Draw single width character
self.write_graphics(x, y, font)
return 1
return 0
def clear(self):
self.text_framebuffer[:] = b' '*64
zeros = bytearray(32)
for gfb in self.graphics_framebuffers:
gfb[:] = zeros
def get_dimensions(self):
return (16, 4)
# Display driver for stock ST7920 displays
class ST7920(DisplayBase):
def __init__(self, config):
printer = config.get_printer()
# pin config
ppins = printer.lookup_object('pins')
pins = [ppins.lookup_pin(config.get(name + '_pin'))
for name in ['cs', 'sclk', 'sid']]
mcu = None
for pin_params in pins:
if mcu is not None and pin_params['chip'] != mcu:
raise ppins.error("""{"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))
+240
View File
@@ -0,0 +1,240 @@
# Support for UC1701 (and similar) 128x64 graphics LCD displays
#
# Copyright (C) 2018-2019 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from .. import bus
from . import font8x14
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
TextGlyphs = { 'right_arrow': b'\x1a', 'degrees': b'\xf8' }
class DisplayBase:
def __init__(self, io, columns=128, x_offset=0):
self.send = io.send
# framebuffers
self.columns = columns
self.x_offset = x_offset
self.vram = [bytearray(self.columns) for i in range(8)]
self.all_framebuffers = [(self.vram[i], bytearray(b'~'*self.columns), i)
for i in range(8)]
# Cache fonts and icons in display byte order
self.font = [self._swizzle_bits(bytearray(c))
for c in font8x14.VGA_FONT]
self.icons = {}
def flush(self):
# Find all differences in the framebuffers and send them to the chip
for new_data, old_data, page in self.all_framebuffers:
if new_data == old_data:
continue
# Find the position of all changed bytes in this framebuffer
diffs = [[i, 1] for i, (n, o) in enumerate(zip(new_data, old_data))
if n != o]
# Batch together changes that are close to each other
for i in range(len(diffs)-2, -1, -1):
pos, count = diffs[i]
nextpos, nextcount = diffs[i+1]
if pos + 5 >= nextpos and nextcount < 16:
diffs[i][1] = nextcount + (nextpos - pos)
del diffs[i+1]
# Transmit changes
for col_pos, count in diffs:
# Set Position registers
ra = 0xb0 | (page & 0x0F)
ca_msb = 0x10 | ((col_pos >> 4) & 0x0F)
ca_lsb = col_pos & 0x0F
self.send([ra, ca_msb, ca_lsb])
# Send Data
self.send(new_data[col_pos:col_pos+count], is_data=True)
old_data[:] = new_data
def _swizzle_bits(self, data):
# Convert from "rows of pixels" format to "columns of pixels"
top = bot = 0
for row in range(8):
spaced = (data[row] * 0x8040201008040201) & 0x8080808080808080
top |= spaced >> (7 - row)
spaced = (data[row + 8] * 0x8040201008040201) & 0x8080808080808080
bot |= spaced >> (7 - row)
bits_top = [(top >> s) & 0xff for s in range(0, 64, 8)]
bits_bot = [(bot >> s) & 0xff for s in range(0, 64, 8)]
return (bytearray(bits_top), bytearray(bits_bot))
def set_glyphs(self, glyphs):
for glyph_name, glyph_data in glyphs.items():
icon = glyph_data.get('icon16x16')
if icon is not None:
top1, bot1 = self._swizzle_bits(icon[0])
top2, bot2 = self._swizzle_bits(icon[1])
self.icons[glyph_name] = (top1 + top2, bot1 + bot2)
def write_text(self, x, y, data):
if x + len(data) > 16:
data = data[:16 - min(x, 16)]
pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1]
for c in bytearray(data):
bits_top, bits_bot = self.font[c]
page_top[pix_x:pix_x+8] = bits_top
page_bot[pix_x:pix_x+8] = bits_bot
pix_x += 8
def write_graphics(self, x, y, data):
if x >= 16 or y >= 4 or len(data) != 16:
return
bits_top, bits_bot = self._swizzle_bits(data)
pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1]
for i in range(8):
page_top[pix_x + i] ^= bits_top[i]
page_bot[pix_x + i] ^= bits_bot[i]
def write_glyph(self, x, y, glyph_name):
icon = self.icons.get(glyph_name)
if icon is not None and x < 15:
# Draw icon in graphics mode
pix_x = x * 8
pix_x += self.x_offset
page_idx = y * 2
self.vram[page_idx][pix_x:pix_x+16] = icon[0]
self.vram[page_idx + 1][pix_x:pix_x+16] = icon[1]
return 2
char = TextGlyphs.get(glyph_name)
if char is not None:
# Draw character
self.write_text(x, y, char)
return 1
return 0
def clear(self):
zeros = bytearray(self.columns)
for page in self.vram:
page[:] = zeros
def get_dimensions(self):
return (16, 4)
# IO wrapper for "4 wire" spi bus (spi bus with an extra data/control line)
class SPI4wire:
def __init__(self, config, data_pin_name):
self.spi = bus.MCU_SPI_from_config(config, 0, default_speed=10000000)
dc_pin = config.get(data_pin_name)
self.mcu_dc = bus.MCU_bus_digital_out(self.spi.get_mcu(), dc_pin,
self.spi.get_command_queue())
def send(self, cmds, is_data=False):
self.mcu_dc.update_digital_out(is_data,
reqclock=BACKGROUND_PRIORITY_CLOCK)
self.spi.spi_send(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
# IO wrapper for i2c bus
class I2C:
def __init__(self, config, default_addr):
self.i2c = bus.MCU_I2C_from_config(config, default_addr=default_addr,
default_speed=400000)
def send(self, cmds, is_data=False):
if is_data:
hdr = 0x40
else:
hdr = 0x00
cmds = bytearray(cmds)
cmds.insert(0, hdr)
self.i2c.i2c_write(cmds, reqclock=BACKGROUND_PRIORITY_CLOCK)
# Helper code for toggling a reset pin on startup
class ResetHelper:
def __init__(self, pin_desc, io_bus):
self.mcu_reset = None
if pin_desc is None:
return
self.mcu_reset = bus.MCU_bus_digital_out(io_bus.get_mcu(), pin_desc,
io_bus.get_command_queue())
def init(self):
if self.mcu_reset is None:
return
mcu = self.mcu_reset.get_mcu()
curtime = mcu.get_printer().get_reactor().monotonic()
print_time = mcu.estimated_print_time(curtime)
# Toggle reset
minclock = mcu.print_time_to_clock(print_time + .100)
self.mcu_reset.update_digital_out(0, minclock=minclock)
minclock = mcu.print_time_to_clock(print_time + .200)
self.mcu_reset.update_digital_out(1, minclock=minclock)
# Force a delay to any subsequent commands on the command queue
minclock = mcu.print_time_to_clock(print_time + .300)
self.mcu_reset.update_digital_out(1, minclock=minclock)
# The UC1701 is a "4-wire" SPI display device
class UC1701(DisplayBase):
def __init__(self, config):
io = SPI4wire(config, "a0_pin")
DisplayBase.__init__(self, io)
self.contrast = config.getint('contrast', 40, minval=0, maxval=63)
self.reset = ResetHelper(config.get("rst_pin", None), io.spi)
def init(self):
self.reset.init()
init_cmds = [0xE2, # System reset
0x40, # Set display to start at line 0
0xA0, # Set SEG direction
0xC8, # Set COM Direction
0xA2, # Set Bias = 1/9
0x2C, # Boost ON
0x2E, # Voltage regulator on
0x2F, # Voltage follower on
0xF8, # Set booster ratio
0x00, # Booster ratio value (4x)
0x23, # Set resistor ratio (3)
0x81, # Set Electronic Volume
self.contrast, # Electronic Volume value
0xAC, # Set static indicator off
0x00, # NOP
0xA6, # Disable Inverse
0xAF] # Set display enable
self.send(init_cmds)
self.send([0xA5]) # display all
self.send([0xA4]) # normal display
self.flush()
# The SSD1306 supports both i2c and "4-wire" spi
class SSD1306(DisplayBase):
def __init__(self, config, columns=128, x_offset=0):
cs_pin = config.get("cs_pin", None)
if cs_pin is None:
io = I2C(config, 60)
io_bus = io.i2c
else:
io = SPI4wire(config, "dc_pin")
io_bus = io.spi
self.reset = ResetHelper(config.get("reset_pin", None), io_bus)
DisplayBase.__init__(self, io, columns, x_offset)
self.contrast = config.getint('contrast', 239, minval=0, maxval=255)
self.vcomh = config.getint('vcomh', 0, minval=0, maxval=63)
self.invert = config.getboolean('invert', False)
def init(self):
self.reset.init()
init_cmds = [
0xAE, # Display off
0xD5, 0x80, # Set oscillator frequency
0xA8, 0x3f, # Set multiplex ratio
0xD3, 0x00, # Set display offset
0x40, # Set display start line
0x8D, 0x14, # Charge pump setting
0x20, 0x02, # Set Memory addressing mode
0xA1, # Set Segment re-map
0xC8, # Set COM output scan direction
0xDA, 0x12, # Set COM pins hardware configuration
0x81, self.contrast, # Set contrast control
0xD9, 0xA1, # Set pre-charge period
0xDB, self.vcomh, # Set VCOMH deselect level
0x2E, # Deactivate scroll
0xA4, # Output ram to display
0xA7 if self.invert else 0xA6, # Set normal/invert
0xAF, # Display on
]
self.send(init_cmds)
self.flush()
# the SH1106 is SSD1306 compatible with up to 132 columns
class SH1106(SSD1306):
def __init__(self, config):
x_offset = config.getint('x_offset', 0, minval=0, maxval=3)
SSD1306.__init__(self, config, 132, x_offset=x_offset)
+50
View File
@@ -0,0 +1,50 @@
# Module to handle M73 and M117 display status commands
#
# Copyright (C) 2018-2020 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2018 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
M73_TIMEOUT = 5.
class DisplayStatus:
def __init__(self, config):
self.printer = config.get_printer()
self.expire_progress = 0.
self.progress = self.message = None
# Register commands
gcode = self.printer.lookup_object('gcode')
gcode.register_command('M73', self.cmd_M73)
gcode.register_command('M117', self.cmd_M117)
gcode.register_command(
'SET_DISPLAY_TEXT', self.cmd_SET_DISPLAY_TEXT,
desc=self.cmd_SET_DISPLAY_TEXT_help)
def get_status(self, eventtime):
progress = self.progress
if progress is not None and eventtime > self.expire_progress:
idle_timeout = self.printer.lookup_object('idle_timeout')
idle_timeout_info = idle_timeout.get_status(eventtime)
if idle_timeout_info['state'] != "Printing":
self.progress = progress = None
if progress is None:
progress = 0.
sdcard = self.printer.lookup_object('virtual_sdcard', None)
if sdcard is not None:
progress = sdcard.get_status(eventtime)['progress']
return { 'progress': progress, 'message': self.message }
def cmd_M73(self, gcmd):
progress = gcmd.get_float('P', None)
if progress is not None:
progress = progress / 100.
self.progress = min(1., max(0., progress))
curtime = self.printer.get_reactor().monotonic()
self.expire_progress = curtime + M73_TIMEOUT
def cmd_M117(self, gcmd):
msg = gcmd.get_raw_command_parameters() or None
self.message = msg
cmd_SET_DISPLAY_TEXT_help = "Set or clear the display message"
def cmd_SET_DISPLAY_TEXT(self, gcmd):
self.message = gcmd.get("MSG", None)
def load_config(config):
return DisplayStatus(config)
+58
View File
@@ -0,0 +1,58 @@
# Support for "dotstar" leds
#
# Copyright (C) 2019-2022 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import bus
BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
class PrinterDotstar:
def __init__(self, config):
self.printer = printer = config.get_printer()
name = config.get_name().split()[1]
# Configure a software spi bus
ppins = printer.lookup_object('pins')
data_pin_params = ppins.lookup_pin(config.get('data_pin'))
clock_pin_params = ppins.lookup_pin(config.get('clock_pin'))
mcu = data_pin_params['chip']
if mcu is not clock_pin_params['chip']:
raise config.error("Dotstar pins must be on same mcu")
sw_spi_pins = (data_pin_params['pin'], data_pin_params['pin'],
clock_pin_params['pin'])
self.spi = bus.MCU_SPI(mcu, None, None, 0, 500000, sw_spi_pins)
# Initialize color data
self.chain_count = config.getint('chain_count', 1, minval=1)
pled = printer.load_object(config, "led")
self.led_helper = pled.setup_helper(config, self.update_leds,
self.chain_count)
self.prev_data = None
# Register commands
printer.register_event_handler("klippy:connect", self.handle_connect)
def handle_connect(self):
self.update_leds(self.led_helper.get_status()['color_data'], None)
def update_leds(self, led_state, print_time):
if led_state == self.prev_data:
return
self.prev_data = led_state
# Build data to send
data = [0] * ((len(led_state) + 2) * 4)
for i, (red, green, blue, white) in enumerate(led_state):
idx = (i + 1) * 4
data[idx] = 0xff
data[idx+1] = int(blue * 255. + .5)
data[idx+2] = int(green * 255. + .5)
data[idx+3] = int(red * 255. + .5)
data[-4] = data[-3] = data[-2] = data[-1] = 0xff
# Transmit update
minclock = 0
if print_time is not None:
minclock = self.spi.get_mcu().print_time_to_clock(print_time)
for d in [data[i:i+20] for i in range(0, len(data), 20)]:
self.spi.spi_send(d, minclock=minclock,
reqclock=BACKGROUND_PRIORITY_CLOCK)
def get_status(self, eventtime):
return self.led_helper.get_status(eventtime)
def load_config_prefix(config):
return PrinterDotstar(config)
+80
View File
@@ -0,0 +1,80 @@
# Support for 1-wire based temperature sensors
#
# Copyright (C) 2020 Alan Lord <alanslists@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import mcu
DS18_REPORT_TIME = 3.0
# Temperature can be sampled at any time but conversion time is ~750ms, so
# setting the time too low will not make the reports come faster.
DS18_MIN_REPORT_TIME = 1.0
DS18_MAX_CONSECUTIVE_ERRORS = 4
class DS18B20:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.sensor_id = bytearray(config.get("serial_no").encode())
self.temp = self.min_temp = self.max_temp = 0.0
self._report_clock = 0
self.report_time = config.getfloat(
'ds18_report_time',
DS18_REPORT_TIME,
minval=DS18_MIN_REPORT_TIME
)
self._mcu = mcu.get_printer_mcu(self.printer, config.get('sensor_mcu'))
self.oid = self._mcu.create_oid()
self._mcu.register_response(self._handle_ds18b20_response,
"ds18b20_result", self.oid)
self._mcu.register_config_callback(self._build_config)
def _build_config(self):
sid = "".join(["%02x" % (x,) for x in self.sensor_id])
self._mcu.add_config_cmd(
"config_ds18b20 oid=%d serial=%s max_error_count=%d"
% (self.oid, sid, DS18_MAX_CONSECUTIVE_ERRORS))
clock = self._mcu.get_query_slot(self.oid)
self._report_clock = self._mcu.seconds_to_clock(self.report_time)
self._mcu.add_config_cmd("query_ds18b20 oid=%d clock=%u rest_ticks=%u"
" min_value=%d max_value=%d" % (
self.oid, clock, self._report_clock,
self.min_temp * 1000, self.max_temp * 1000), is_init=True)
def _handle_ds18b20_response(self, params):
temp = params['value'] / 1000.0
if params["fault"]:
logging.info("ds18b20 reports fault %d (temp=%0.1f)",
params["fault"], temp)
return
next_clock = self._mcu.clock32_to_clock64(params['next_clock'])
last_read_clock = next_clock - self._report_clock
last_read_time = self._mcu.clock_to_print_time(last_read_clock)
self._callback(last_read_time, temp)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
def fault(self, msg):
self.printer.invoke_async_shutdown(msg)
def get_report_time_delta(self):
return self.report_time
def setup_callback(self, cb):
self._callback = cb
def get_status(self, eventtime):
return {
'temperature': round(self.temp, 2),
}
def load_config(config):
# Register sensor
pheaters = config.get_printer().load_object(config, "heaters")
pheaters.add_sensor_factory("DS18B20", DS18B20)
+15
View File
@@ -0,0 +1,15 @@
# Tool to disable config checks for duplicate pins
#
# Copyright (C) 2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class PrinterDupPinOverride:
def __init__(self, config):
printer = config.get_printer()
ppins = printer.lookup_object('pins')
for pin_desc in config.getlist('pins'):
ppins.allow_multi_use_pin(pin_desc)
def load_config(config):
return PrinterDupPinOverride(config)
+237
View File
@@ -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)
+311
View File
@@ -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)
+22
View File
@@ -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)
+121
View File
@@ -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)
+116
View File
@@ -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)
+28
View File
@@ -0,0 +1,28 @@
# Support fans that are controlled by gcode
#
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import fan
class PrinterFanGeneric:
cmd_SET_FAN_SPEED_help = "Sets the speed of a fan"
def __init__(self, config):
self.printer = config.get_printer()
self.fan = fan.Fan(config, default_shutdown_speed=0.)
self.fan_name = config.get_name().split()[-1]
gcode = self.printer.lookup_object("gcode")
gcode.register_mux_command("SET_FAN_SPEED", "FAN",
self.fan_name,
self.cmd_SET_FAN_SPEED,
desc=self.cmd_SET_FAN_SPEED_help)
def get_status(self, eventtime):
return self.fan.get_status(eventtime)
def cmd_SET_FAN_SPEED(self, gcmd):
speed = gcmd.get_float('SPEED', 0.)
self.fan.set_speed_from_command(speed)
def load_config_prefix(config):
return PrinterFanGeneric(config)
+77
View File
@@ -0,0 +1,77 @@
# Filament Motion Sensor Module
#
# Copyright (C) 2021 Joshua Wherrett <thejoshw.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import filament_switch_sensor
CHECK_RUNOUT_TIMEOUT = .250
class EncoderSensor:
def __init__(self, config):
# Read config
self.printer = config.get_printer()
switch_pin = config.get('switch_pin')
self.extruder_name = config.get('extruder')
self.detection_length = config.getfloat(
'detection_length', 7., above=0.)
# Configure pins
buttons = self.printer.load_object(config, 'buttons')
buttons.register_buttons([switch_pin], self.encoder_event)
# Get printer objects
self.reactor = self.printer.get_reactor()
self.runout_helper = filament_switch_sensor.RunoutHelper(config)
self.get_status = self.runout_helper.get_status
self.extruder = None
self.estimated_print_time = None
# Initialise internal state
self.filament_runout_pos = None
# Register commands and event handlers
self.printer.register_event_handler('klippy:ready',
self._handle_ready)
self.printer.register_event_handler('idle_timeout:printing',
self._handle_printing)
self.printer.register_event_handler('idle_timeout:ready',
self._handle_not_printing)
self.printer.register_event_handler('idle_timeout:idle',
self._handle_not_printing)
def _update_filament_runout_pos(self, eventtime=None):
if eventtime is None:
eventtime = self.reactor.monotonic()
self.filament_runout_pos = (
self._get_extruder_pos(eventtime) +
self.detection_length)
def _handle_ready(self):
self.extruder = self.printer.lookup_object(self.extruder_name)
self.estimated_print_time = (
self.printer.lookup_object('mcu').estimated_print_time)
self._update_filament_runout_pos()
self._extruder_pos_update_timer = self.reactor.register_timer(
self._extruder_pos_update_event)
def _handle_printing(self, print_time):
self.reactor.update_timer(self._extruder_pos_update_timer,
self.reactor.NOW)
def _handle_not_printing(self, print_time):
self.reactor.update_timer(self._extruder_pos_update_timer,
self.reactor.NEVER)
def _get_extruder_pos(self, eventtime=None):
if eventtime is None:
eventtime = self.reactor.monotonic()
print_time = self.estimated_print_time(eventtime)
return self.extruder.find_past_position(print_time)
def _extruder_pos_update_event(self, eventtime):
extruder_pos = self._get_extruder_pos(eventtime)
# Check for filament runout
self.runout_helper.note_filament_present(
extruder_pos < self.filament_runout_pos)
return eventtime + CHECK_RUNOUT_TIMEOUT
def encoder_event(self, eventtime, state):
if self.extruder is not None:
self._update_filament_runout_pos(eventtime)
# Check for filament insertion
# Filament is always assumed to be present on an encoder event
self.runout_helper.note_filament_present(True)
def load_config_prefix(config):
return EncoderSensor(config)
+120
View File
@@ -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)
+125
View File
@@ -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)
+74
View File
@@ -0,0 +1,74 @@
# Support for Marlin/Smoothie/Reprap style firmware retraction via G10/G11
#
# Copyright (C) 2019 Len Trigg <lenbok@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class FirmwareRetraction:
def __init__(self, config):
self.printer = config.get_printer()
self.retract_length = config.getfloat('retract_length', 0., minval=0.)
self.retract_speed = config.getfloat('retract_speed', 20., minval=1)
self.unretract_extra_length = config.getfloat(
'unretract_extra_length', 0., minval=0.)
self.unretract_speed = config.getfloat('unretract_speed', 10., minval=1)
self.unretract_length = (self.retract_length
+ self.unretract_extra_length)
self.is_retracted = False
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command('SET_RETRACTION', self.cmd_SET_RETRACTION,
desc=self.cmd_SET_RETRACTION_help)
self.gcode.register_command('GET_RETRACTION', self.cmd_GET_RETRACTION,
desc=self.cmd_GET_RETRACTION_help)
self.gcode.register_command('G10', self.cmd_G10)
self.gcode.register_command('G11', self.cmd_G11)
def get_status(self, eventtime):
return {
"retract_length": self.retract_length,
"retract_speed": self.retract_speed,
"unretract_extra_length": self.unretract_extra_length,
"unretract_speed": self.unretract_speed,
}
cmd_SET_RETRACTION_help = ("Set firmware retraction parameters")
def cmd_SET_RETRACTION(self, gcmd):
self.retract_length = gcmd.get_float('RETRACT_LENGTH',
self.retract_length, minval=0.)
self.retract_speed = gcmd.get_float('RETRACT_SPEED',
self.retract_speed, minval=1)
self.unretract_extra_length = gcmd.get_float(
'UNRETRACT_EXTRA_LENGTH', self.unretract_extra_length, minval=0.)
self.unretract_speed = gcmd.get_float('UNRETRACT_SPEED',
self.unretract_speed, minval=1)
self.unretract_length = (self.retract_length
+ self.unretract_extra_length)
self.is_retracted = False
cmd_GET_RETRACTION_help = ("Report firmware retraction paramters")
def cmd_GET_RETRACTION(self, gcmd):
gcmd.respond_info("RETRACT_LENGTH=%.5f RETRACT_SPEED=%.5f"
" UNRETRACT_EXTRA_LENGTH=%.5f UNRETRACT_SPEED=%.5f"
% (self.retract_length, self.retract_speed,
self.unretract_extra_length, self.unretract_speed))
def cmd_G10(self, gcmd):
if not self.is_retracted:
self.gcode.run_script_from_command(
"SAVE_GCODE_STATE NAME=_retract_state\n"
"G91\n"
"G1 E-%.5f F%d\n"
"RESTORE_GCODE_STATE NAME=_retract_state"
% (self.retract_length, self.retract_speed*60))
self.is_retracted = True
def cmd_G11(self, gcmd):
if self.is_retracted:
self.gcode.run_script_from_command(
"SAVE_GCODE_STATE NAME=_retract_state\n"
"G91\n"
"G1 E%.5f F%d\n"
"RESTORE_GCODE_STATE NAME=_retract_state"
% (self.unretract_length, self.unretract_speed*60))
self.is_retracted = False
def load_config(config):
return FirmwareRetraction(config)
+136
View File
@@ -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)
+181
View File
@@ -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)
+51
View File
@@ -0,0 +1,51 @@
# Support for executing gcode when a hardware button is pressed or released.
#
# Copyright (C) 2019 Alec Plumb <alec@etherwalker.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
class GCodeButton:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split(' ')[-1]
self.pin = config.get('pin')
self.last_state = 0
buttons = self.printer.load_object(config, "buttons")
if config.get('analog_range', None) is None:
buttons.register_buttons([self.pin], self.button_callback)
else:
amin, amax = config.getfloatlist('analog_range', count=2)
pullup = config.getfloat('analog_pullup_resistor', 4700., above=0.)
buttons.register_adc_button(self.pin, amin, amax, pullup,
self.button_callback)
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.press_template = gcode_macro.load_template(config, 'press_gcode')
self.release_template = gcode_macro.load_template(config,
'release_gcode', '')
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_mux_command("QUERY_BUTTON", "BUTTON", self.name,
self.cmd_QUERY_BUTTON,
desc=self.cmd_QUERY_BUTTON_help)
cmd_QUERY_BUTTON_help = "Report on the state of a button"
def cmd_QUERY_BUTTON(self, gcmd):
gcmd.respond_info(self.name + ": " + self.get_status()['state'])
def button_callback(self, eventtime, state):
self.last_state = state
template = self.press_template
if not state:
template = self.release_template
try:
self.gcode.run_script(template.render())
except:
logging.exception("Script running error")
def get_status(self, eventtime=None):
if self.last_state:
return {'state': "PRESSED"}
return {'state': "RELEASED"}
def load_config_prefix(config):
return GCodeButton(config)
+221
View File
@@ -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)
+493
View File
@@ -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)
+224
View File
@@ -0,0 +1,224 @@
# Support for filament width sensor
#
# Copyright (C) 2019 Mustafa YILDIZ <mydiz@hotmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import filament_switch_sensor
ADC_REPORT_TIME = 0.500
ADC_SAMPLE_TIME = 0.03
ADC_SAMPLE_COUNT = 15
class HallFilamentWidthSensor:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.pin1 = config.get('adc1')
self.pin2 = config.get('adc2')
self.dia1=config.getfloat('Cal_dia1', 1.5)
self.dia2=config.getfloat('Cal_dia2', 2.0)
self.rawdia1=config.getint('Raw_dia1', 9500)
self.rawdia2=config.getint('Raw_dia2', 10500)
self.MEASUREMENT_INTERVAL_MM=config.getint('measurement_interval',10)
self.nominal_filament_dia = config.getfloat(
'default_nominal_filament_diameter', above=1)
self.measurement_delay = config.getfloat('measurement_delay', above=0.)
self.measurement_max_difference = config.getfloat('max_difference', 0.2)
self.max_diameter = (self.nominal_filament_dia
+ self.measurement_max_difference)
self.min_diameter = (self.nominal_filament_dia
- self.measurement_max_difference)
self.diameter =self.nominal_filament_dia
self.is_active =config.getboolean('enable', False)
self.runout_dia=config.getfloat('min_diameter', 1.0)
self.is_log =config.getboolean('logging', False)
# Use the current diameter instead of nominal while the first
# measurement isn't in place
self.use_current_dia_while_delay = config.getboolean(
'use_current_dia_while_delay', False)
# filament array [position, filamentWidth]
self.filament_array = []
self.lastFilamentWidthReading = 0
self.lastFilamentWidthReading2 = 0
self.firstExtruderUpdatePosition = 0
self.filament_width = self.nominal_filament_dia
# printer objects
self.toolhead = self.ppins = self.mcu_adc = None
self.printer.register_event_handler("klippy:ready", self.handle_ready)
# Start adc
self.ppins = self.printer.lookup_object('pins')
self.mcu_adc = self.ppins.setup_pin('adc', self.pin1)
self.mcu_adc.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc.setup_adc_callback(ADC_REPORT_TIME, self.adc_callback)
self.mcu_adc2 = self.ppins.setup_pin('adc', self.pin2)
self.mcu_adc2.setup_minmax(ADC_SAMPLE_TIME, ADC_SAMPLE_COUNT)
self.mcu_adc2.setup_adc_callback(ADC_REPORT_TIME, self.adc2_callback)
# extrude factor updating
self.extrude_factor_update_timer = self.reactor.register_timer(
self.extrude_factor_update_event)
# Register commands
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command('QUERY_FILAMENT_WIDTH', self.cmd_M407)
self.gcode.register_command('RESET_FILAMENT_WIDTH_SENSOR',
self.cmd_ClearFilamentArray)
self.gcode.register_command('DISABLE_FILAMENT_WIDTH_SENSOR',
self.cmd_M406)
self.gcode.register_command('ENABLE_FILAMENT_WIDTH_SENSOR',
self.cmd_M405)
self.gcode.register_command('QUERY_RAW_FILAMENT_WIDTH',
self.cmd_Get_Raw_Values)
self.gcode.register_command('ENABLE_FILAMENT_WIDTH_LOG',
self.cmd_log_enable)
self.gcode.register_command('DISABLE_FILAMENT_WIDTH_LOG',
self.cmd_log_disable)
self.runout_helper = filament_switch_sensor.RunoutHelper(config)
# Initialization
def handle_ready(self):
# Load printer objects
self.toolhead = self.printer.lookup_object('toolhead')
# Start extrude factor update timer
self.reactor.update_timer(self.extrude_factor_update_timer,
self.reactor.NOW)
def adc_callback(self, read_time, read_value):
# read sensor value
self.lastFilamentWidthReading = round(read_value * 10000)
def adc2_callback(self, read_time, read_value):
# read sensor value
self.lastFilamentWidthReading2 = round(read_value * 10000)
# calculate diameter
diameter_new = round((self.dia2 - self.dia1)/
(self.rawdia2-self.rawdia1)*
((self.lastFilamentWidthReading+self.lastFilamentWidthReading2)
-self.rawdia1)+self.dia1,2)
self.diameter=(5.0 * self.diameter + diameter_new)/6
def update_filament_array(self, last_epos):
# Fill array
if len(self.filament_array) > 0:
# Get last reading position in array & calculate next
# reading position
next_reading_position = (self.filament_array[-1][0] +
self.MEASUREMENT_INTERVAL_MM)
if next_reading_position <= (last_epos + self.measurement_delay):
self.filament_array.append([last_epos + self.measurement_delay,
self.diameter])
if self.is_log:
self.gcode.respond_info("Filament width:%.3f" %
( self.diameter ))
else:
# add first item to array
self.filament_array.append([self.measurement_delay + last_epos,
self.diameter])
self.firstExtruderUpdatePosition = (self.measurement_delay
+ last_epos)
def extrude_factor_update_event(self, eventtime):
# Update extrude factor
pos = self.toolhead.get_position()
last_epos = pos[3]
# Update filament array for lastFilamentWidthReading
self.update_filament_array(last_epos)
# Check runout
self.runout_helper.note_filament_present(
self.diameter > self.runout_dia)
# Does filament exists
if self.diameter > 0.5:
if len(self.filament_array) > 0:
# Get first position in filament array
pending_position = self.filament_array[0][0]
if pending_position <= last_epos:
# Get first item in filament_array queue
item = self.filament_array.pop(0)
self.filament_width = item[1]
else:
if ((self.use_current_dia_while_delay)
and (self.firstExtruderUpdatePosition
== pending_position)):
self.filament_width = self.diameter
elif self.firstExtruderUpdatePosition == pending_position:
self.filament_width = self.nominal_filament_dia
if ((self.filament_width <= self.max_diameter)
and (self.filament_width >= self.min_diameter)):
percentage = round(self.nominal_filament_dia**2
/ self.filament_width**2 * 100)
self.gcode.run_script("M221 S" + str(percentage))
else:
self.gcode.run_script("M221 S100")
else:
self.gcode.run_script("M221 S100")
self.filament_array = []
if self.is_active:
return eventtime + 1
else:
return self.reactor.NEVER
def cmd_M407(self, gcmd):
response = ""
if self.diameter > 0:
response += ("Filament dia (measured mm): "
+ str(self.diameter))
else:
response += "Filament NOT present"
gcmd.respond_info(response)
def cmd_ClearFilamentArray(self, gcmd):
self.filament_array = []
gcmd.respond_info("Filament width measurements cleared!")
# Set extrude multiplier to 100%
self.gcode.run_script_from_command("M221 S100")
def cmd_M405(self, gcmd):
response = "Filament width sensor Turned On"
if self.is_active:
response = "Filament width sensor is already On"
else:
self.is_active = True
# Start extrude factor update timer
self.reactor.update_timer(self.extrude_factor_update_timer,
self.reactor.NOW)
gcmd.respond_info(response)
def cmd_M406(self, gcmd):
response = "Filament width sensor Turned Off"
if not self.is_active:
response = "Filament width sensor is already Off"
else:
self.is_active = False
# Stop extrude factor update timer
self.reactor.update_timer(self.extrude_factor_update_timer,
self.reactor.NEVER)
# Clear filament array
self.filament_array = []
# Set extrude multiplier to 100%
self.gcode.run_script_from_command("M221 S100")
gcmd.respond_info(response)
def cmd_Get_Raw_Values(self, gcmd):
response = "ADC1="
response += (" "+str(self.lastFilamentWidthReading))
response += (" ADC2="+str(self.lastFilamentWidthReading2))
response += (" RAW="+
str(self.lastFilamentWidthReading
+self.lastFilamentWidthReading2))
gcmd.respond_info(response)
def get_status(self, eventtime):
return {'Diameter': self.diameter,
'Raw':(self.lastFilamentWidthReading+
self.lastFilamentWidthReading2),
'is_active':self.is_active}
def cmd_log_enable(self, gcmd):
self.is_log = True
gcmd.respond_info("Filament width logging Turned On")
def cmd_log_disable(self, gcmd):
self.is_log = False
gcmd.respond_info("Filament width logging Turned Off")
def load_config(config):
return HallFilamentWidthSensor(config)
+28
View File
@@ -0,0 +1,28 @@
# Support for a heated bed
#
# Copyright (C) 2018-2019 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class PrinterHeaterBed:
def __init__(self, config):
self.printer = config.get_printer()
pheaters = self.printer.load_object(config, 'heaters')
self.heater = pheaters.setup_heater(config, 'B')
self.get_status = self.heater.get_status
self.stats = self.heater.stats
# Register commands
gcode = self.printer.lookup_object('gcode')
gcode.register_command("M140", self.cmd_M140)
gcode.register_command("M190", self.cmd_M190)
def cmd_M140(self, gcmd, wait=False):
# Set Bed Temperature
temp = gcmd.get_float('S', 0.)
pheaters = self.printer.lookup_object('heaters')
pheaters.set_temperature(self.heater, temp, wait)
def cmd_M190(self, gcmd):
# Set Bed Temperature and Wait
self.cmd_M140(gcmd, wait=True)
def load_config(config):
return PrinterHeaterBed(config)
+42
View File
@@ -0,0 +1,42 @@
# Support fans that are enabled when a heater is on
#
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from . import fan
PIN_MIN_TIME = 0.100
class PrinterHeaterFan:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.load_object(config, 'heaters')
self.printer.register_event_handler("klippy:ready", self.handle_ready)
self.heater_names = config.getlist("heater", ("extruder",))
self.heater_temp = config.getfloat("heater_temp", 50.0)
self.heaters = []
self.fan = fan.Fan(config, default_shutdown_speed=1.)
self.fan_speed = config.getfloat("fan_speed", 1., minval=0., maxval=1.)
self.last_speed = 0.
def handle_ready(self):
pheaters = self.printer.lookup_object('heaters')
self.heaters = [pheaters.lookup_heater(n) for n in self.heater_names]
reactor = self.printer.get_reactor()
reactor.register_timer(self.callback, reactor.monotonic()+PIN_MIN_TIME)
def get_status(self, eventtime):
return self.fan.get_status(eventtime)
def callback(self, eventtime):
speed = 0.
for heater in self.heaters:
current_temp, target_temp = heater.get_temp(eventtime)
if target_temp or current_temp > self.heater_temp:
speed = self.fan_speed
if speed != self.last_speed:
self.last_speed = speed
curtime = self.printer.get_reactor().monotonic()
print_time = self.fan.get_mcu().estimated_print_time(curtime)
self.fan.set_speed(print_time + PIN_MIN_TIME, speed)
return eventtime + 1.
def load_config_prefix(config):
return PrinterHeaterFan(config)
+9
View File
@@ -0,0 +1,9 @@
# Support for a generic heater
#
# Copyright (C) 2019 John Jardine <john@gprime.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
def load_config_prefix(config):
pheaters = config.get_printer().load_object(config, 'heaters')
return pheaters.setup_heater(config)
+488
View File
@@ -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)
+277
View File
@@ -0,0 +1,277 @@
# Helper code for implementing homing operations
#
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math
HOMING_START_DELAY = 0.001
ENDSTOP_SAMPLE_TIME = .000015
ENDSTOP_SAMPLE_COUNT = 4
# Return a completion that completes when all completions in a list complete
def multi_complete(printer, completions):
if len(completions) == 1:
return completions[0]
# Build completion that waits for all completions
reactor = printer.get_reactor()
cp = reactor.register_callback(lambda e: [c.wait() for c in completions])
# If any completion indicates an error, then exit main completion early
for c in completions:
reactor.register_callback(
lambda e, c=c: cp.complete(1) if c.wait() else 0)
return cp
# Tracking of stepper positions during a homing/probing move
class StepperPosition:
def __init__(self, stepper, endstop_name):
self.stepper = stepper
self.endstop_name = endstop_name
self.stepper_name = stepper.get_name()
self.start_pos = stepper.get_mcu_position()
self.halt_pos = self.trig_pos = None
def note_home_end(self, trigger_time):
self.halt_pos = self.stepper.get_mcu_position()
self.trig_pos = self.stepper.get_past_mcu_position(trigger_time)
# Implementation of homing/probing moves
class HomingMove:
def __init__(self, printer, endstops, toolhead=None):
self.printer = printer
self.endstops = endstops
if toolhead is None:
toolhead = printer.lookup_object('toolhead')
self.toolhead = toolhead
self.stepper_positions = []
def get_mcu_endstops(self):
return [es for es, name in self.endstops]
def _calc_endstop_rate(self, mcu_endstop, movepos, speed):
startpos = self.toolhead.get_position()
axes_d = [mp - sp for mp, sp in zip(movepos, startpos)]
move_d = math.sqrt(sum([d*d for d in axes_d[:3]]))
move_t = move_d / speed
max_steps = max([(abs(s.calc_position_from_coord(startpos)
- s.calc_position_from_coord(movepos))
/ s.get_step_dist())
for s in mcu_endstop.get_steppers()])
if max_steps <= 0.:
return .001
return move_t / max_steps
def calc_toolhead_pos(self, kin_spos, offsets):
kin_spos = dict(kin_spos)
kin = self.toolhead.get_kinematics()
for stepper in kin.get_steppers():
sname = stepper.get_name()
kin_spos[sname] += offsets.get(sname, 0) * stepper.get_step_dist()
thpos = self.toolhead.get_position()
return list(kin.calc_position(kin_spos))[:3] + thpos[3:]
def homing_move(self, movepos, speed, probe_pos=False,
triggered=True, check_triggered=True):
# Notify start of homing/probing move
self.printer.send_event("homing:homing_move_begin", self)
# Note start location
self.toolhead.flush_step_generation()
kin = self.toolhead.get_kinematics()
kin_spos = {s.get_name(): s.get_commanded_position()
for s in kin.get_steppers()}
self.stepper_positions = [ StepperPosition(s, name)
for es, name in self.endstops
for s in es.get_steppers() ]
# Start endstop checking
print_time = self.toolhead.get_last_move_time()
endstop_triggers = []
for mcu_endstop, name in self.endstops:
rest_time = self._calc_endstop_rate(mcu_endstop, movepos, speed)
wait = mcu_endstop.home_start(print_time, ENDSTOP_SAMPLE_TIME,
ENDSTOP_SAMPLE_COUNT, rest_time,
triggered=triggered)
endstop_triggers.append(wait)
all_endstop_trigger = multi_complete(self.printer, endstop_triggers)
self.toolhead.dwell(HOMING_START_DELAY)
# Issue move
error = None
try:
self.toolhead.drip_move(movepos, speed, all_endstop_trigger)
except self.printer.command_error as e:
error = """{"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)
+64
View File
@@ -0,0 +1,64 @@
# Heater handling on homing moves
#
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
class HomingHeaters:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
self.printer.register_event_handler("homing:homing_move_begin",
self.handle_homing_move_begin)
self.printer.register_event_handler("homing:homing_move_end",
self.handle_homing_move_end)
self.disable_heaters = config.getlist("heaters", None)
self.flaky_steppers = config.getlist("steppers", None)
self.pheaters = self.printer.load_object(config, 'heaters')
self.target_save = {}
def handle_connect(self):
# heaters to disable
all_heaters = self.pheaters.get_all_heaters()
if self.disable_heaters is None:
self.disable_heaters = all_heaters
else:
if not all(x in all_heaters for x in self.disable_heaters):
raise self.printer.config_error(
"""{"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)
+65
View File
@@ -0,0 +1,65 @@
# Run user defined actions in place of a normal G28 homing command
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class HomingOverride:
def __init__(self, config):
self.printer = config.get_printer()
self.start_pos = [config.getfloat('set_position_' + a, None)
for a in 'xyz']
self.axes = config.get('axes', 'XYZ').upper()
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.template = gcode_macro.load_template(config, 'gcode')
self.in_script = False
self.printer.load_object(config, 'homing')
self.gcode = self.printer.lookup_object('gcode')
self.prev_G28 = self.gcode.register_command("G28", None)
self.gcode.register_command("G28", self.cmd_G28)
def cmd_G28(self, gcmd):
if self.in_script:
# Was called recursively - invoke the real G28 command
self.prev_G28(gcmd)
return
# if no axis is given as parameter we assume the override
no_axis = True
for axis in 'XYZ':
if gcmd.get(axis, None) is not None:
no_axis = False
break
if no_axis:
override = True
else:
# check if we home an axis which needs the override
override = False
for axis in self.axes:
if gcmd.get(axis, None) is not None:
override = True
if not override:
self.prev_G28(gcmd)
return
# Calculate forced position (if configured)
toolhead = self.printer.lookup_object('toolhead')
pos = toolhead.get_position()
homing_axes = []
for axis, loc in enumerate(self.start_pos):
if loc is not None:
pos[axis] = loc
homing_axes.append(axis)
toolhead.set_position(pos, homing_axes=homing_axes)
# Perform homing
context = self.template.create_template_context()
context['params'] = gcmd.get_command_parameters()
try:
self.in_script = True
self.template.run_gcode_from_command(context)
finally:
self.in_script = False
def load_config(config):
return HomingOverride(config)
+248
View File
@@ -0,0 +1,248 @@
# HTU21D(F)/Si7013/Si7020/Si7021/SHT21 i2c based temperature sensors support
#
# Copyright (C) 2020 Lucio Tarantino <lucio.tarantino@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import bus
######################################################################
# NOTE: The implementation requires write support of length 0
# before reading on the i2c bus of the mcu.
#
# Compatible Sensors:
# HTU21D - Tested on Linux MCU.
# Si7013 - Untested
# Si7020 - Untested
# Si7021 - Tested on Pico MCU
# SHT21 - Untested
#
######################################################################
HTU21D_I2C_ADDR= 0x40
HTU21D_COMMANDS = {
'HTU21D_TEMP' :0xE3,
'HTU21D_HUMID' :0xE5,
'HTU21D_TEMP_NH' :0xF3,
'HTU21D_HUMID_NH' :0xF5,
'WRITE' :0xE6,
'READ' :0xE7,
'RESET' :0xFE,
'SERIAL' :[0xFA,0x0F,0xFC,0xC9],
'FIRMWARE_READ' :[0x84,0xB8]
}
HTU21D_RESOLUTION_MASK = 0x7E;
HTU21D_RESOLUTIONS = {
'TEMP14_HUM12':int('00000000',2),
'TEMP13_HUM10':int('10000000',2),
'TEMP12_HUM08':int('00000001',2),
'TEMP11_HUM11':int('10000001',2)
}
# Device with conversion time for tmp/resolution bit
# The format is:
# <CHIPNAME>:{id:<ID>, ..<RESOlUTION>:[<temp time>,<humidity time>].. }
HTU21D_DEVICES = {
'SI7013':{'id':0x0D,
'TEMP14_HUM12':[.11,.12],
'TEMP13_HUM10':[ .7, .5],
'TEMP12_HUM08':[ .4, .4],
'TEMP11_HUM11':[ .3, .7]},
'SI7020':{'id':0x14,
'TEMP14_HUM12':[.11,.12],
'TEMP13_HUM10':[ .7, .5],
'TEMP12_HUM08':[ .4, .4],
'TEMP11_HUM11':[ .3, .7]},
'SI7021':{'id':0x15,
'TEMP14_HUM12':[.11,.12],
'TEMP13_HUM10':[ .7, .5],
'TEMP12_HUM08':[ .4, .4],
'TEMP11_HUM11':[ .3, .7]},
'SHT21': {'id':0x31,
'TEMP14_HUM12':[.85,.29],
'TEMP13_HUM10':[.43, .9],
'TEMP12_HUM08':[.22, .4],
'TEMP11_HUM11':[.11,.15]},
'HTU21D':{'id':0x32,
'TEMP14_HUM12':[.50,.16],
'TEMP13_HUM10':[.25, .5],
'TEMP12_HUM08':[.13, .3],
'TEMP11_HUM11':[.12, .8]}
}
#temperature coefficient for RH compensation at range 0C..80C,
# for HTU21D & SHT21 only
HTU21D_TEMP_COEFFICIENT= -0.15
#crc8 polynomial for 16bit value, CRC8 -> x^8 + x^5 + x^4 + 1
HTU21D_CRC8_POLYNOMINAL= 0x13100
class HTU21D:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.reactor = self.printer.get_reactor()
self.i2c = bus.MCU_I2C_from_config(
config, default_addr=HTU21D_I2C_ADDR, default_speed=100000)
self.hold_master_mode = config.getboolean('htu21d_hold_master',False)
self.resolution = config.get('htu21d_resolution','TEMP12_HUM08')
self.report_time = config.getint('htu21d_report_time',30,minval=5)
if self.resolution not in HTU21D_RESOLUTIONS:
raise config.error("""{"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)
+207
View File
@@ -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)
+116
View File
@@ -0,0 +1,116 @@
# Support for disabling the printer on an idle timeout
#
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
DEFAULT_IDLE_GCODE = """
{% if 'heaters' in printer %}
TURN_OFF_HEATERS
{% endif %}
M84
"""
PIN_MIN_TIME = 0.100
READY_TIMEOUT = .500
class IdleTimeout:
def __init__(self, config):
self.printer = config.get_printer()
self.reactor = self.printer.get_reactor()
self.gcode = self.printer.lookup_object('gcode')
self.toolhead = self.timeout_timer = None
self.printer.register_event_handler("klippy:ready", self.handle_ready)
self.idle_timeout = config.getfloat('timeout', 600., above=0.)
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.idle_gcode = gcode_macro.load_template(config, 'gcode',
DEFAULT_IDLE_GCODE)
self.gcode.register_command('SET_IDLE_TIMEOUT',
self.cmd_SET_IDLE_TIMEOUT,
desc=self.cmd_SET_IDLE_TIMEOUT_help)
self.state = "Idle"
self.last_print_start_systime = 0.
def get_status(self, eventtime):
printing_time = 0.
if self.state == "Printing":
printing_time = eventtime - self.last_print_start_systime
return { "state": self.state, "printing_time": printing_time }
def handle_ready(self):
self.toolhead = self.printer.lookup_object('toolhead')
self.timeout_timer = self.reactor.register_timer(self.timeout_handler)
self.printer.register_event_handler("toolhead:sync_print_time",
self.handle_sync_print_time)
def transition_idle_state(self, eventtime):
self.state = "Printing"
try:
script = self.idle_gcode.render()
res = self.gcode.run_script(script)
except:
logging.exception("idle timeout gcode execution")
self.state = "Ready"
return eventtime + 1.
print_time = self.toolhead.get_last_move_time()
self.state = "Idle"
self.printer.send_event("idle_timeout:idle", print_time)
return self.reactor.NEVER
def check_idle_timeout(self, eventtime):
# Make sure toolhead class isn't busy
print_time, est_print_time, lookahead_empty = self.toolhead.check_busy(
eventtime)
idle_time = est_print_time - print_time
if not lookahead_empty or idle_time < 1.:
# Toolhead is busy
return eventtime + self.idle_timeout
if idle_time < self.idle_timeout:
# Wait for idle timeout
return eventtime + self.idle_timeout - idle_time
if self.gcode.get_mutex().test():
# Gcode class busy
return eventtime + 1.
# Idle timeout has elapsed
return self.transition_idle_state(eventtime)
def timeout_handler(self, eventtime):
if self.printer.is_shutdown():
return self.reactor.NEVER
if self.state == "Ready":
return self.check_idle_timeout(eventtime)
# Check if need to transition to "ready" state
print_time, est_print_time, lookahead_empty = self.toolhead.check_busy(
eventtime)
buffer_time = min(2., print_time - est_print_time)
if not lookahead_empty:
# Toolhead is busy
return eventtime + READY_TIMEOUT + max(0., buffer_time)
if buffer_time > -READY_TIMEOUT:
# Wait for ready timeout
return eventtime + READY_TIMEOUT + buffer_time
if self.gcode.get_mutex().test():
# Gcode class busy
return eventtime + READY_TIMEOUT
# Transition to "ready" state
self.state = "Ready"
self.printer.send_event("idle_timeout:ready",
est_print_time + PIN_MIN_TIME)
return eventtime + self.idle_timeout
def handle_sync_print_time(self, curtime, print_time, est_print_time):
if self.state == "Printing":
return
# Transition to "printing" state
self.state = "Printing"
self.last_print_start_systime = curtime
check_time = READY_TIMEOUT + print_time - est_print_time
self.reactor.update_timer(self.timeout_timer, curtime + check_time)
self.printer.send_event("idle_timeout:printing",
est_print_time + PIN_MIN_TIME)
cmd_SET_IDLE_TIMEOUT_help = "Set the idle timeout in seconds"
def cmd_SET_IDLE_TIMEOUT(self, gcmd):
timeout = gcmd.get_float('TIMEOUT', self.idle_timeout, above=0.)
self.idle_timeout = timeout
gcmd.respond_info("idle_timeout: Timeout set to %.2f s" % (timeout,))
if self.state == "Ready":
checktime = self.reactor.monotonic() + timeout
self.reactor.update_timer(self.timeout_timer, checktime)
def load_config(config):
return IdleTimeout(config)
+171
View File
@@ -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)
+108
View File
@@ -0,0 +1,108 @@
# Support for I2C based LM75/LM75A temperature sensors
#
# Copyright (C) 2020 Boleslaw Ciesielski <combolek@users.noreply.github.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
from . import bus
LM75_CHIP_ADDR = 0x48
LM75_I2C_SPEED = 100000
LM75_REGS = {
'TEMP' : 0x00,
'CONF' : 0x01,
'THYST' : 0x02,
'TOS' : 0x03,
'PRODID' : 0x07 # TI LM75A chips only?
}
LM75_REPORT_TIME = .8
# Temperature can be sampled at any time but the read aborts
# the current conversion. Conversion time is 300ms so make
# sure not to read too often.
LM75_MIN_REPORT_TIME = .5
class LM75:
def __init__(self, config):
self.printer = config.get_printer()
self.name = config.get_name().split()[-1]
self.reactor = self.printer.get_reactor()
self.i2c = bus.MCU_I2C_from_config(config, LM75_CHIP_ADDR,
LM75_I2C_SPEED)
self.mcu = self.i2c.get_mcu()
self.report_time = config.getfloat('lm75_report_time', LM75_REPORT_TIME,
minval=LM75_MIN_REPORT_TIME)
self.temp = self.min_temp = self.max_temp = 0.0
self.sample_timer = self.reactor.register_timer(self._sample_lm75)
self.printer.add_object("lm75 " + self.name, self)
self.printer.register_event_handler("klippy:connect",
self.handle_connect)
def handle_connect(self):
self._init_lm75()
self.reactor.update_timer(self.sample_timer, self.reactor.NOW)
def setup_minmax(self, min_temp, max_temp):
self.min_temp = min_temp
self.max_temp = max_temp
def setup_callback(self, cb):
self._callback = cb
def get_report_time_delta(self):
return self.report_time
def degrees_from_sample(self, x):
# The temp sample is encoded in the top 9 bits of a 16-bit
# value. Resolution is 0.5 degrees C.
return x[0] + (x[1] >> 7) * 0.5
def _init_lm75(self):
# Check and report the chip ID but ignore errors since many
# chips don't have it
try:
prodid = self.read_register('PRODID', 1)[0]
logging.info("lm75: Chip ID %#x" % prodid)
except:
pass
def _sample_lm75(self, eventtime):
try:
sample = self.read_register('TEMP', 2)
self.temp = self.degrees_from_sample(sample)
except Exception:
logging.exception("lm75: Error reading data")
self.temp = 0.0
return self.reactor.NEVER
if self.temp < self.min_temp or self.temp > self.max_temp:
self.printer.invoke_shutdown(
"""{"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)
+262
View File
@@ -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)
+128
View File
@@ -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)
+1196
View File
File diff suppressed because it is too large Load Diff
+333
View File
@@ -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)
+54
View File
@@ -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)
+112
View File
@@ -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)
+144
View File
@@ -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)
+209
View File
@@ -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)
+144
View File
@@ -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)
+155
View File
@@ -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)
+471
View File
@@ -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))
+21
View File
@@ -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.
+79
View File
@@ -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
+126
View File
@@ -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)
+35
View File
@@ -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)
+45
View File
@@ -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)
+271
View File
@@ -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)
+376
View File
@@ -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)
+55
View File
@@ -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)
+90
View File
@@ -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)
+41
View File
@@ -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)
+64
View File
@@ -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)
+73
View File
@@ -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)
+473
View File
@@ -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))
+102
View File
@@ -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.),
]
+162
View File
@@ -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)
+154
View File
@@ -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
+166
View File
@@ -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)
+355
View File
@@ -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)
+17
View File
@@ -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)
+74
View File
@@ -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)
+133
View File
@@ -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)
+185
View File
@@ -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)
+80
View File
@@ -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)
+173
View File
@@ -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)
+42
View File
@@ -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)
+109
View File
@@ -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
+107
View File
@@ -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)
+563
View File
@@ -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