mirror of
https://github.com/QIDITECH/klipper.git
synced 2026-02-02 17:08:41 +03:00
plus4的klipper版本
This commit is contained in:
Binary file not shown.
@@ -95,8 +95,8 @@ class LinearVoltage:
|
||||
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())
|
||||
logging.warning("Ignoring adc sample %.3f/%.3f in heater %s",
|
||||
temp, volt, config.get_name())
|
||||
continue
|
||||
samples.append((adc, temp))
|
||||
try:
|
||||
|
||||
Binary file not shown.
@@ -1,10 +1,10 @@
|
||||
# Support for reading acceleration data from an adxl345 chip
|
||||
#
|
||||
# Copyright (C) 2020-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2020-2023 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 logging, time, collections, multiprocessing, os
|
||||
from . import bus, bulk_sensor
|
||||
|
||||
# ADXL345 registers
|
||||
REG_DEVID = 0x00
|
||||
@@ -24,33 +24,37 @@ ADXL345_DEV_ID = 0xe5
|
||||
SET_FIFO_CTL = 0x90
|
||||
|
||||
FREEFALL_ACCEL = 9.80665 * 1000.
|
||||
SCALE = 0.0039 * FREEFALL_ACCEL # 3.9mg/LSB * Earth gravity in mm/s**2
|
||||
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):
|
||||
def __init__(self, printer):
|
||||
self.printer = printer
|
||||
self.cconn = cconn
|
||||
self.is_finished = False
|
||||
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 = []
|
||||
self.msgs = []
|
||||
self.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
|
||||
self.is_finished = True
|
||||
def handle_batch(self, msg):
|
||||
if self.is_finished:
|
||||
return False
|
||||
if len(self.msgs) >= 10000:
|
||||
# Avoid filling up memory with too many samples
|
||||
return False
|
||||
self.msgs.append(msg)
|
||||
return True
|
||||
def has_valid_samples(self):
|
||||
raw_samples = self._get_raw_samples()
|
||||
for msg in raw_samples:
|
||||
data = msg['params']['data']
|
||||
for msg in self.msgs:
|
||||
data = msg['data']
|
||||
first_sample_time = data[0][0]
|
||||
last_sample_time = data[-1][0]
|
||||
if (first_sample_time > self.request_end_time
|
||||
@@ -59,21 +63,20 @@ class AccelQueryHelper:
|
||||
# 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
|
||||
# of the samples from msgs 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:
|
||||
if not self.msgs:
|
||||
return self.samples
|
||||
total = sum([len(m['params']['data']) for m in raw_samples])
|
||||
total = sum([len(m['data']) for m in self.msgs])
|
||||
count = 0
|
||||
self.samples = samples = [None] * total
|
||||
for msg in raw_samples:
|
||||
for samp_time, x, y, z in msg['params']['data']:
|
||||
for msg in self.msgs:
|
||||
for samp_time, x, y, z in msg['data']:
|
||||
if samp_time < self.request_start_time:
|
||||
continue
|
||||
if samp_time > self.request_end_time:
|
||||
@@ -172,77 +175,31 @@ class AccelCommandHelper:
|
||||
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
|
||||
# Helper to read the axes_map parameter from the config
|
||||
def read_axes_map(config):
|
||||
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("Invalid axes_map parameter")
|
||||
return [am[a.strip()] for a in axes_map]
|
||||
|
||||
MIN_MSG_TIME = 0.100
|
||||
|
||||
BYTES_PER_SAMPLE = 5
|
||||
SAMPLES_PER_BLOCK = 10
|
||||
|
||||
BATCH_UPDATES = 0.100
|
||||
|
||||
# Printer class that controls ADXL345 chip
|
||||
class ADXL345:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
AccelCommandHelper(config, self)
|
||||
self.query_rate = 0
|
||||
am = {'x': (0, SCALE), 'y': (1, SCALE), 'z': (2, SCALE),
|
||||
'-x': (0, -SCALE), '-y': (1, -SCALE), '-z': (2, -SCALE)}
|
||||
axes_map = config.getlist('axes_map', ('x','y','z'), count=3)
|
||||
if any([a not in am for a in axes_map]):
|
||||
raise config.error("Invalid adxl345 axes_map parameter")
|
||||
self.axes_map = [am[a.strip()] for a in axes_map]
|
||||
self.axes_map = read_axes_map(config)
|
||||
self.data_rate = config.getint('rate', 3200)
|
||||
if self.data_rate not in QUERY_RATES:
|
||||
raise config.error("Invalid rate parameter: %d" % (self.data_rate,))
|
||||
# Measurement storage (accessed from background thread)
|
||||
self.lock = threading.Lock()
|
||||
self.raw_samples = []
|
||||
# Setup mcu sensor_adxl345 bulk query code
|
||||
self.spi = bus.MCU_SPI_from_config(config, 3, default_speed=5000000)
|
||||
self.mcu = mcu = self.spi.get_mcu()
|
||||
@@ -254,18 +211,21 @@ class ADXL345:
|
||||
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)
|
||||
self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, "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)
|
||||
chip_smooth = self.data_rate * BATCH_UPDATES * 2
|
||||
self.clock_sync = bulk_sensor.ClockSyncRegression(mcu, chip_smooth)
|
||||
self.clock_updater = bulk_sensor.ChipClockUpdater(self.clock_sync,
|
||||
BYTES_PER_SAMPLE)
|
||||
self.last_error_count = 0
|
||||
# Process messages in batches
|
||||
self.batch_bulk = bulk_sensor.BatchBulkHelper(
|
||||
self.printer, self._process_batch,
|
||||
self._start_measurements, self._finish_measurements, BATCH_UPDATES)
|
||||
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)
|
||||
hdr = ('time', 'x_acceleration', 'y_acceleration', 'z_acceleration')
|
||||
self.batch_bulk.add_mux_endpoint("adxl345/dump_adxl345", "sensor",
|
||||
self.name, {'header': hdr})
|
||||
def _build_config(self):
|
||||
cmdqueue = self.spi.get_command_queue()
|
||||
self.query_adxl345_cmd = self.mcu.lookup_command(
|
||||
@@ -291,24 +251,23 @@ class ADXL345:
|
||||
"This is generally indicative of connection problems "
|
||||
"(e.g. faulty wiring) or a faulty adxl345 chip." % (
|
||||
reg, val, stored_val))
|
||||
# Measurement collection
|
||||
def is_measuring(self):
|
||||
return self.query_rate > 0
|
||||
def _handle_adxl345_data(self, params):
|
||||
with self.lock:
|
||||
self.raw_samples.append(params)
|
||||
def start_internal_client(self):
|
||||
aqh = AccelQueryHelper(self.printer)
|
||||
self.batch_bulk.add_client(aqh.handle_batch)
|
||||
return aqh
|
||||
# Measurement decoding
|
||||
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
|
||||
last_sequence = self.clock_updater.get_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 = (params['sequence'] - last_sequence) & 0xffff
|
||||
seq_diff -= (seq_diff & 0x8000) << 1
|
||||
seq = last_sequence - seq_diff
|
||||
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):
|
||||
@@ -341,33 +300,9 @@ class ADXL345:
|
||||
break
|
||||
else:
|
||||
raise self.printer.command_error("Unable to query adxl345 fifo")
|
||||
mcu_clock = self.mcu.clock32_to_clock64(params['clock'])
|
||||
sequence = (self.last_sequence & ~0xffff) | params['next_sequence']
|
||||
if sequence < self.last_sequence:
|
||||
sequence += 0x10000
|
||||
self.last_sequence = sequence
|
||||
buffered = params['buffered']
|
||||
limit_count = (self.last_limit_count & ~0xffff) | params['limit_count']
|
||||
if limit_count < self.last_limit_count:
|
||||
limit_count += 0x10000
|
||||
self.last_limit_count = limit_count
|
||||
duration = params['query_ticks']
|
||||
if duration > self.max_query_duration:
|
||||
# Skip measurement as a high query time could skew clock tracking
|
||||
self.max_query_duration = max(2 * self.max_query_duration,
|
||||
self.mcu.seconds_to_clock(.000005))
|
||||
return
|
||||
self.max_query_duration = 2 * duration
|
||||
msg_count = (sequence * SAMPLES_PER_BLOCK
|
||||
+ buffered // BYTES_PER_SAMPLE + fifo)
|
||||
# The "chip clock" is the message counter plus .5 for average
|
||||
# inaccuracy of query responses and plus .5 for assumed offset
|
||||
# of adxl345 hw processing time.
|
||||
chip_clock = msg_count + 1
|
||||
self.clock_sync.update(mcu_clock + duration // 2, chip_clock)
|
||||
self.clock_updater.update_clock(params)
|
||||
# Start, stop, and process message batches
|
||||
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)
|
||||
@@ -383,59 +318,35 @@ class ADXL345:
|
||||
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
|
||||
self.bulk_queue.clear_samples()
|
||||
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.clock_updater.note_start(reqclock)
|
||||
self._update_clock(minclock=reqclock)
|
||||
self.max_query_duration = 1 << 31
|
||||
self.clock_updater.clear_duration_filter()
|
||||
self.last_error_count = 0
|
||||
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 = []
|
||||
self.bulk_queue.clear_samples()
|
||||
logging.info("ADXL345 finished '%s' measurements", self.name)
|
||||
# API interface
|
||||
def _api_update(self, eventtime):
|
||||
def _process_batch(self, eventtime):
|
||||
self._update_clock()
|
||||
with self.lock:
|
||||
raw_samples = self.raw_samples
|
||||
self.raw_samples = []
|
||||
raw_samples = self.bulk_queue.pull_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)
|
||||
'overflows': self.clock_updater.get_last_limit_count()}
|
||||
|
||||
def load_config(config):
|
||||
return ADXL345(config)
|
||||
|
||||
Binary file not shown.
162
klippy/extras/aht10.py
Normal file
162
klippy/extras/aht10.py
Normal file
@@ -0,0 +1,162 @@
|
||||
# AHT10/AHT20/AHT21 I2c-based humiditure sensor support
|
||||
#
|
||||
# Copyright (C) 2023 Scott Mudge <mail@scottmudge.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
from . import bus
|
||||
|
||||
######################################################################
|
||||
# Compatible Sensors:
|
||||
# AHT10 - Tested w/ BTT GTR 1.0 MCU on i2c3
|
||||
# AHT20 - Untested but should work
|
||||
# AHT21 - Tested w/ BTT GTR 1.0 MCU on i2c3
|
||||
######################################################################
|
||||
|
||||
AHT10_I2C_ADDR= 0x38
|
||||
|
||||
AHT10_COMMANDS = {
|
||||
'INIT' :[0xE1, 0x08, 0x00],
|
||||
'MEASURE' :[0xAC, 0x33, 0x00],
|
||||
'RESET' :[0xBA, 0x08, 0x00]
|
||||
}
|
||||
|
||||
AHT10_MAX_BUSY_CYCLES= 5
|
||||
|
||||
class AHT10:
|
||||
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=AHT10_I2C_ADDR, default_speed=100000)
|
||||
self.report_time = config.getint('aht10_report_time',30,minval=5)
|
||||
self.temp = self.min_temp = self.max_temp = self.humidity = 0.
|
||||
self.sample_timer = self.reactor.register_timer(self._sample_aht10)
|
||||
self.printer.add_object("aht10 " + self.name, self)
|
||||
self.printer.register_event_handler("klippy:connect",
|
||||
self.handle_connect)
|
||||
self.is_calibrated = False
|
||||
self.init_sent = False
|
||||
|
||||
def handle_connect(self):
|
||||
self._init_aht10()
|
||||
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 _make_measurement(self):
|
||||
if not self.init_sent:
|
||||
return False
|
||||
|
||||
data = None
|
||||
|
||||
is_busy = True
|
||||
cycles = 0
|
||||
|
||||
try:
|
||||
while is_busy:
|
||||
# Check if we're constantly busy. If so, send soft-reset
|
||||
# and issue warning.
|
||||
if is_busy and cycles > AHT10_MAX_BUSY_CYCLES:
|
||||
logging.warning("aht10: device reported busy after " +
|
||||
"%d cycles, resetting device"% AHT10_MAX_BUSY_CYCLES)
|
||||
self._reset_device()
|
||||
data = None
|
||||
break
|
||||
|
||||
cycles += 1
|
||||
# Write command for updating temperature+status bit
|
||||
self.i2c.i2c_write(AHT10_COMMANDS['MEASURE'])
|
||||
# Wait 110ms after first read, 75ms minimum
|
||||
self.reactor.pause(self.reactor.monotonic() + .110)
|
||||
|
||||
# Read data
|
||||
read = self.i2c.i2c_read([], 6)
|
||||
if read is None:
|
||||
logging.warning("aht10: received data from" +
|
||||
" i2c_read is None")
|
||||
continue
|
||||
data = bytearray(read['response'])
|
||||
if len(data) < 6:
|
||||
logging.warning("aht10: received bytes less than" +
|
||||
" expected 6 [%d]"%len(data))
|
||||
continue
|
||||
|
||||
self.is_calibrated = True if (data[0] & 0b00000100) else False
|
||||
is_busy = True if (data[0] & 0b01000000) else False
|
||||
|
||||
if is_busy:
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.exception("aht10: exception encountered" +
|
||||
" reading data: %s"%str(e))
|
||||
return False
|
||||
|
||||
temp = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]
|
||||
self.temp = ((temp*200) / 1048576) - 50
|
||||
hum = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4
|
||||
self.humidity = int(hum * 100 / 1048576)
|
||||
|
||||
# Clamp humidity
|
||||
if (self.humidity > 100):
|
||||
self.humidity = 100
|
||||
elif (self.humidity < 0):
|
||||
self.humidity = 0
|
||||
|
||||
return True
|
||||
|
||||
def _reset_device(self):
|
||||
if not self.init_sent:
|
||||
return
|
||||
|
||||
# Reset device
|
||||
self.i2c.i2c_write(AHT10_COMMANDS['RESET'])
|
||||
# Wait 100ms after reset
|
||||
self.reactor.pause(self.reactor.monotonic() + .10)
|
||||
|
||||
def _init_aht10(self):
|
||||
# Init device
|
||||
self.i2c.i2c_write(AHT10_COMMANDS['INIT'])
|
||||
# Wait 100ms after init
|
||||
self.reactor.pause(self.reactor.monotonic() + .10)
|
||||
self.init_sent = True
|
||||
|
||||
if self._make_measurement():
|
||||
logging.info("aht10: successfully initialized, initial temp: " +
|
||||
"%.3f, humidity: %.3f"%(self.temp, self.humidity))
|
||||
|
||||
def _sample_aht10(self, eventtime):
|
||||
if not self._make_measurement():
|
||||
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(
|
||||
"AHT10 temperature %0.1f outside range of %0.1f:%.01f"
|
||||
% (self.temp, self.min_temp, self.max_temp))
|
||||
|
||||
measured_time = self.reactor.monotonic()
|
||||
print_time = self.i2c.get_mcu().estimated_print_time(measured_time)
|
||||
self._callback(print_time, self.temp)
|
||||
return measured_time + self.report_time
|
||||
|
||||
def 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")
|
||||
pheater.add_sensor_factory("AHT10", AHT10)
|
||||
@@ -3,13 +3,14 @@
|
||||
# Copyright (C) 2021,2022 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging, math, threading
|
||||
from . import bus, motion_report
|
||||
import logging, math
|
||||
from . import bus, bulk_sensor
|
||||
|
||||
MIN_MSG_TIME = 0.100
|
||||
TCODE_ERROR = 0xff
|
||||
|
||||
TRINAMIC_DRIVERS = ["tmc2130", "tmc2208", "tmc2209", "tmc2240", "tmc2660", "tmc5160"]
|
||||
TRINAMIC_DRIVERS = ["tmc2130", "tmc2208", "tmc2209", "tmc2240", "tmc2660",
|
||||
"tmc5160"]
|
||||
|
||||
CALIBRATION_BITS = 6 # 64 entries
|
||||
ANGLE_BITS = 16 # angles range from 0..65535
|
||||
@@ -84,9 +85,9 @@ class AngleCalibration:
|
||||
cal2 = calibration[bucket + 1]
|
||||
adj = (angle & interp_mask) * (cal2 - cal1)
|
||||
adj = cal1 + ((adj + interp_round) >> interp_bits)
|
||||
angle_diff = (angle - adj) & 0xffff
|
||||
angle_diff = (adj - angle) & 0xffff
|
||||
angle_diff -= (angle_diff & 0x8000) << 1
|
||||
new_angle = angle - angle_diff
|
||||
new_angle = angle + angle_diff
|
||||
if calibration_reversed:
|
||||
new_angle = -new_angle
|
||||
samples[i] = (samp_time, new_angle)
|
||||
@@ -156,8 +157,14 @@ class AngleCalibration:
|
||||
def do_calibration_moves(self):
|
||||
move = self.printer.lookup_object('force_move').manual_move
|
||||
# Start data collection
|
||||
angle_sensor = self.printer.lookup_object(self.name)
|
||||
cconn = angle_sensor.start_internal_client()
|
||||
msgs = []
|
||||
is_finished = False
|
||||
def handle_batch(msg):
|
||||
if is_finished:
|
||||
return False
|
||||
msgs.append(msg)
|
||||
return True
|
||||
self.printer.lookup_object(self.name).add_client(handle_batch)
|
||||
# Move stepper several turns (to allow internal sensor calibration)
|
||||
microsteps, full_steps = self.get_microsteps()
|
||||
mcu_stepper = self.mcu_stepper
|
||||
@@ -189,13 +196,12 @@ class AngleCalibration:
|
||||
move(mcu_stepper, .5*rotation_dist + align_dist, move_speed)
|
||||
toolhead.wait_moves()
|
||||
# Finish data collection
|
||||
cconn.finalize()
|
||||
msgs = cconn.get_messages()
|
||||
is_finished = True
|
||||
# Correlate query responses
|
||||
cal = {}
|
||||
step = 0
|
||||
for msg in msgs:
|
||||
for query_time, pos in msg['params']['data']:
|
||||
for query_time, pos in msg['data']:
|
||||
# Add to step tracking
|
||||
while step < len(times) and query_time > times[step][1]:
|
||||
step += 1
|
||||
@@ -374,9 +380,9 @@ class HelperTLE5012B:
|
||||
mcu_clock, chip_clock = self._query_clock()
|
||||
mdiff = mcu_clock - self.last_chip_mcu_clock
|
||||
chip_mclock = self.last_chip_clock + int(mdiff * self.chip_freq + .5)
|
||||
cdiff = (chip_mclock - chip_clock) & 0xffff
|
||||
cdiff = (chip_clock - chip_mclock) & 0xffff
|
||||
cdiff -= (cdiff & 0x8000) << 1
|
||||
new_chip_clock = chip_mclock - cdiff
|
||||
new_chip_clock = chip_mclock + cdiff
|
||||
self.chip_freq = float(new_chip_clock - self.last_chip_clock) / mdiff
|
||||
self.last_chip_clock = new_chip_clock
|
||||
self.last_chip_mcu_clock = mcu_clock
|
||||
@@ -405,7 +411,11 @@ class HelperTLE5012B:
|
||||
parser=lambda x: int(x, 0))
|
||||
self._write_reg(reg, val)
|
||||
|
||||
BYTES_PER_SAMPLE = 3
|
||||
SAMPLES_PER_BLOCK = 16
|
||||
|
||||
SAMPLE_PERIOD = 0.000400
|
||||
BATCH_UPDATES = 0.100
|
||||
|
||||
class Angle:
|
||||
def __init__(self, config):
|
||||
@@ -416,9 +426,6 @@ class Angle:
|
||||
# Measurement conversion
|
||||
self.start_clock = self.time_shift = self.sample_ticks = 0
|
||||
self.last_sequence = self.last_angle = 0
|
||||
# Measurement storage (accessed from background thread)
|
||||
self.lock = threading.Lock()
|
||||
self.raw_samples = []
|
||||
# Sensor type
|
||||
sensors = { "a1333": HelperA1333, "as5047d": HelperAS5047D,
|
||||
"tle5012b": HelperTLE5012B }
|
||||
@@ -438,15 +445,15 @@ class Angle:
|
||||
"query_spi_angle oid=%d clock=0 rest_ticks=0 time_shift=0"
|
||||
% (oid,), on_restart=True)
|
||||
mcu.register_config_callback(self._build_config)
|
||||
mcu.register_response(self._handle_spi_angle_data,
|
||||
"spi_angle_data", oid)
|
||||
# API server endpoints
|
||||
self.api_dump = motion_report.APIDumpHelper(
|
||||
self.printer, self._api_update, self._api_startstop, 0.100)
|
||||
self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, "spi_angle_data", oid)
|
||||
# Process messages in batches
|
||||
self.batch_bulk = bulk_sensor.BatchBulkHelper(
|
||||
self.printer, self._process_batch,
|
||||
self._start_measurements, self._finish_measurements, BATCH_UPDATES)
|
||||
self.name = config.get_name().split()[1]
|
||||
wh = self.printer.lookup_object('webhooks')
|
||||
wh.register_mux_endpoint("angle/dump_angle", "sensor", self.name,
|
||||
self._handle_dump_angle)
|
||||
api_resp = {'header': ('time', 'angle')}
|
||||
self.batch_bulk.add_mux_endpoint("angle/dump_angle",
|
||||
"sensor", self.name, api_resp)
|
||||
def _build_config(self):
|
||||
freq = self.mcu.seconds_to_clock(1.)
|
||||
while float(TCODE_ERROR << self.time_shift) / freq < 0.002:
|
||||
@@ -460,12 +467,9 @@ class Angle:
|
||||
"spi_angle_end oid=%c sequence=%hu", oid=self.oid, cq=cmdqueue)
|
||||
def get_status(self, eventtime=None):
|
||||
return {'temperature': self.sensor_helper.last_temperature}
|
||||
# Measurement collection
|
||||
def is_measuring(self):
|
||||
return self.start_clock != 0
|
||||
def _handle_spi_angle_data(self, params):
|
||||
with self.lock:
|
||||
self.raw_samples.append(params)
|
||||
def add_client(self, client_cb):
|
||||
self.batch_bulk.add_client(client_cb)
|
||||
# Measurement decoding
|
||||
def _extract_samples(self, raw_samples):
|
||||
# Load variables to optimize inner loop below
|
||||
sample_ticks = self.sample_ticks
|
||||
@@ -486,23 +490,23 @@ class Angle:
|
||||
static_delay = self.sensor_helper.get_static_delay()
|
||||
# Process every message in raw_samples
|
||||
count = error_count = 0
|
||||
samples = [None] * (len(raw_samples) * 16)
|
||||
samples = [None] * (len(raw_samples) * SAMPLES_PER_BLOCK)
|
||||
for params in raw_samples:
|
||||
seq = (last_sequence & ~0xffff) | params['sequence']
|
||||
if seq < last_sequence:
|
||||
seq += 0x10000
|
||||
last_sequence = seq
|
||||
seq_diff = (params['sequence'] - last_sequence) & 0xffff
|
||||
last_sequence += seq_diff
|
||||
samp_count = last_sequence * SAMPLES_PER_BLOCK
|
||||
msg_mclock = start_clock + samp_count*sample_ticks
|
||||
d = bytearray(params['data'])
|
||||
msg_mclock = start_clock + seq*16*sample_ticks
|
||||
for i in range(len(d) // 3):
|
||||
tcode = d[i*3]
|
||||
for i in range(len(d) // BYTES_PER_SAMPLE):
|
||||
d_ta = d[i*BYTES_PER_SAMPLE:(i+1)*BYTES_PER_SAMPLE]
|
||||
tcode = d_ta[0]
|
||||
if tcode == TCODE_ERROR:
|
||||
error_count += 1
|
||||
continue
|
||||
raw_angle = d[i*3 + 1] | (d[i*3 + 2] << 8)
|
||||
angle_diff = (last_angle - raw_angle) & 0xffff
|
||||
raw_angle = d_ta[1] | (d_ta[2] << 8)
|
||||
angle_diff = (raw_angle - last_angle) & 0xffff
|
||||
angle_diff -= (angle_diff & 0x8000) << 1
|
||||
last_angle -= angle_diff
|
||||
last_angle += angle_diff
|
||||
mclock = msg_mclock + i*sample_ticks
|
||||
if is_tcode_absolute:
|
||||
# tcode is tle5012b frame counter
|
||||
@@ -521,29 +525,14 @@ class Angle:
|
||||
self.last_angle = last_angle
|
||||
del samples[count:]
|
||||
return samples, error_count
|
||||
# API interface
|
||||
def _api_update(self, eventtime):
|
||||
if self.sensor_helper.is_tcode_absolute:
|
||||
self.sensor_helper.update_clock()
|
||||
with self.lock:
|
||||
raw_samples = self.raw_samples
|
||||
self.raw_samples = []
|
||||
if not raw_samples:
|
||||
return {}
|
||||
samples, error_count = self._extract_samples(raw_samples)
|
||||
if not samples:
|
||||
return {}
|
||||
offset = self.calibration.apply_calibration(samples)
|
||||
return {'data': samples, 'errors': error_count,
|
||||
'position_offset': offset}
|
||||
# Start, stop, and process message batches
|
||||
def _is_measuring(self):
|
||||
return self.start_clock != 0
|
||||
def _start_measurements(self):
|
||||
if self.is_measuring():
|
||||
return
|
||||
logging.info("Starting angle '%s' measurements", self.name)
|
||||
self.sensor_helper.start()
|
||||
# Start bulk reading
|
||||
with self.lock:
|
||||
self.raw_samples = []
|
||||
self.bulk_queue.clear_samples()
|
||||
self.last_sequence = 0
|
||||
systime = self.printer.get_reactor().monotonic()
|
||||
print_time = self.mcu.estimated_print_time(systime) + MIN_MSG_TIME
|
||||
@@ -553,26 +542,23 @@ class Angle:
|
||||
self.query_spi_angle_cmd.send([self.oid, reqclock, rest_ticks,
|
||||
self.time_shift], reqclock=reqclock)
|
||||
def _finish_measurements(self):
|
||||
if not self.is_measuring():
|
||||
return
|
||||
# Halt bulk reading
|
||||
params = self.query_spi_angle_end_cmd.send([self.oid, 0, 0, 0])
|
||||
self.start_clock = 0
|
||||
with self.lock:
|
||||
self.raw_samples = []
|
||||
self.bulk_queue.clear_samples()
|
||||
self.sensor_helper.last_temperature = None
|
||||
logging.info("Stopped angle '%s' measurements", self.name)
|
||||
def _api_startstop(self, is_start):
|
||||
if is_start:
|
||||
self._start_measurements()
|
||||
else:
|
||||
self._finish_measurements()
|
||||
def _handle_dump_angle(self, web_request):
|
||||
self.api_dump.add_client(web_request)
|
||||
hdr = ('time', 'angle')
|
||||
web_request.send({'header': hdr})
|
||||
def start_internal_client(self):
|
||||
return self.api_dump.add_internal_client()
|
||||
def _process_batch(self, eventtime):
|
||||
if self.sensor_helper.is_tcode_absolute:
|
||||
self.sensor_helper.update_clock()
|
||||
raw_samples = self.bulk_queue.pull_samples()
|
||||
if not raw_samples:
|
||||
return {}
|
||||
samples, error_count = self._extract_samples(raw_samples)
|
||||
if not samples:
|
||||
return {}
|
||||
offset = self.calibration.apply_calibration(samples)
|
||||
return {'data': samples, 'errors': error_count,
|
||||
'position_offset': offset}
|
||||
|
||||
def load_config_prefix(config):
|
||||
return Angle(config)
|
||||
|
||||
258
klippy/extras/axis_twist_compensation.py
Normal file
258
klippy/extras/axis_twist_compensation.py
Normal file
@@ -0,0 +1,258 @@
|
||||
# Axis Twist Compensation
|
||||
#
|
||||
# Copyright (C) 2022 Jeremy Tan <jeremytkw98@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
import math
|
||||
from . import manual_probe as ManualProbe, bed_mesh as BedMesh
|
||||
|
||||
|
||||
DEFAULT_SAMPLE_COUNT = 3
|
||||
DEFAULT_SPEED = 50.
|
||||
DEFAULT_HORIZONTAL_MOVE_Z = 5.
|
||||
|
||||
|
||||
class AxisTwistCompensation:
|
||||
def __init__(self, config):
|
||||
# get printer
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
|
||||
# get values from [axis_twist_compensation] section in printer .cfg
|
||||
self.horizontal_move_z = config.getfloat('horizontal_move_z',
|
||||
DEFAULT_HORIZONTAL_MOVE_Z)
|
||||
self.speed = config.getfloat('speed', DEFAULT_SPEED)
|
||||
self.calibrate_start_x = config.getfloat('calibrate_start_x')
|
||||
self.calibrate_end_x = config.getfloat('calibrate_end_x')
|
||||
self.calibrate_y = config.getfloat('calibrate_y')
|
||||
self.z_compensations = config.getlists('z_compensations',
|
||||
default=[], parser=float)
|
||||
self.compensation_start_x = config.getfloat('compensation_start_x',
|
||||
default=None)
|
||||
self.compensation_end_x = config.getfloat('compensation_start_y',
|
||||
default=None)
|
||||
|
||||
self.m = None
|
||||
self.b = None
|
||||
|
||||
# setup calibrater
|
||||
self.calibrater = Calibrater(self, config)
|
||||
|
||||
def get_z_compensation_value(self, pos):
|
||||
if not self.z_compensations:
|
||||
return 0
|
||||
|
||||
x_coord = pos[0]
|
||||
z_compensations = self.z_compensations
|
||||
sample_count = len(z_compensations)
|
||||
spacing = ((self.calibrate_end_x - self.calibrate_start_x)
|
||||
/ (sample_count - 1))
|
||||
interpolate_t = (x_coord - self.calibrate_start_x) / spacing
|
||||
interpolate_i = int(math.floor(interpolate_t))
|
||||
interpolate_i = BedMesh.constrain(interpolate_i, 0, sample_count - 2)
|
||||
interpolate_t -= interpolate_i
|
||||
interpolated_z_compensation = BedMesh.lerp(
|
||||
interpolate_t, z_compensations[interpolate_i],
|
||||
z_compensations[interpolate_i + 1])
|
||||
return interpolated_z_compensation
|
||||
|
||||
def clear_compensations(self):
|
||||
self.z_compensations = []
|
||||
self.m = None
|
||||
self.b = None
|
||||
|
||||
|
||||
class Calibrater:
|
||||
def __init__(self, compensation, config):
|
||||
# setup self attributes
|
||||
self.compensation = compensation
|
||||
self.printer = compensation.printer
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.probe = None
|
||||
# probe settings are set to none, until they are available
|
||||
self.lift_speed, self.probe_x_offset, self.probe_y_offset, _ = \
|
||||
None, None, None, None
|
||||
self.printer.register_event_handler("klippy:connect",
|
||||
self._handle_connect)
|
||||
self.speed = compensation.speed
|
||||
self.horizontal_move_z = compensation.horizontal_move_z
|
||||
self.start_point = (compensation.calibrate_start_x,
|
||||
compensation.calibrate_y)
|
||||
self.end_point = (compensation.calibrate_end_x,
|
||||
compensation.calibrate_y)
|
||||
self.results = None
|
||||
self.current_point_index = None
|
||||
self.gcmd = None
|
||||
self.configname = config.get_name()
|
||||
|
||||
# register gcode handlers
|
||||
self._register_gcode_handlers()
|
||||
|
||||
def _handle_connect(self):
|
||||
self.probe = self.printer.lookup_object('probe', None)
|
||||
if (self.probe is None):
|
||||
config = self.printer.lookup_object('configfile')
|
||||
raise config.error(
|
||||
"AXIS_TWIST_COMPENSATION requires [probe] to be defined")
|
||||
self.lift_speed = self.probe.get_lift_speed()
|
||||
self.probe_x_offset, self.probe_y_offset, _ = \
|
||||
self.probe.get_offsets()
|
||||
|
||||
def _register_gcode_handlers(self):
|
||||
# register gcode handlers
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.gcode.register_command(
|
||||
'AXIS_TWIST_COMPENSATION_CALIBRATE',
|
||||
self.cmd_AXIS_TWIST_COMPENSATION_CALIBRATE,
|
||||
desc=self.cmd_AXIS_TWIST_COMPENSATION_CALIBRATE_help)
|
||||
|
||||
cmd_AXIS_TWIST_COMPENSATION_CALIBRATE_help = """
|
||||
Performs the x twist calibration wizard
|
||||
Measure z probe offset at n points along the x axis,
|
||||
and calculate x twist compensation
|
||||
"""
|
||||
|
||||
def cmd_AXIS_TWIST_COMPENSATION_CALIBRATE(self, gcmd):
|
||||
self.gcmd = gcmd
|
||||
sample_count = gcmd.get_int('SAMPLE_COUNT', DEFAULT_SAMPLE_COUNT)
|
||||
|
||||
# check for valid sample_count
|
||||
if sample_count is None or sample_count < 2:
|
||||
raise self.gcmd.error(
|
||||
"SAMPLE_COUNT to probe must be at least 2")
|
||||
|
||||
# clear the current config
|
||||
self.compensation.clear_compensations()
|
||||
|
||||
# calculate some values
|
||||
x_range = self.end_point[0] - self.start_point[0]
|
||||
interval_dist = x_range / (sample_count - 1)
|
||||
nozzle_points = self._calculate_nozzle_points(sample_count,
|
||||
interval_dist)
|
||||
probe_points = self._calculate_probe_points(
|
||||
nozzle_points, self.probe_x_offset, self.probe_y_offset)
|
||||
|
||||
# verify no other manual probe is in progress
|
||||
ManualProbe.verify_no_manual_probe(self.printer)
|
||||
|
||||
# begin calibration
|
||||
self.current_point_index = 0
|
||||
self.results = []
|
||||
self._calibration(probe_points, nozzle_points, interval_dist)
|
||||
|
||||
def _calculate_nozzle_points(self, sample_count, interval_dist):
|
||||
# calculate the points to put the probe at, returned as a list of tuples
|
||||
nozzle_points = []
|
||||
for i in range(sample_count):
|
||||
x = self.start_point[0] + i * interval_dist
|
||||
y = self.start_point[1]
|
||||
nozzle_points.append((x, y))
|
||||
return nozzle_points
|
||||
|
||||
def _calculate_probe_points(self, nozzle_points,
|
||||
probe_x_offset, probe_y_offset):
|
||||
# calculate the points to put the nozzle at
|
||||
# returned as a list of tuples
|
||||
probe_points = []
|
||||
for point in nozzle_points:
|
||||
x = point[0] - probe_x_offset
|
||||
y = point[1] - probe_y_offset
|
||||
probe_points.append((x, y))
|
||||
return probe_points
|
||||
|
||||
def _move_helper(self, target_coordinates, override_speed=None):
|
||||
# pad target coordinates
|
||||
target_coordinates = \
|
||||
(target_coordinates[0], target_coordinates[1], None) \
|
||||
if len(target_coordinates) == 2 else target_coordinates
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
speed = self.speed if target_coordinates[2] == None else self.lift_speed
|
||||
speed = override_speed if override_speed is not None else speed
|
||||
toolhead.manual_move(target_coordinates, speed)
|
||||
|
||||
def _calibration(self, probe_points, nozzle_points, interval):
|
||||
# begin the calibration process
|
||||
self.gcmd.respond_info("AXIS_TWIST_COMPENSATION_CALIBRATE: "
|
||||
"Probing point %d of %d" % (
|
||||
self.current_point_index + 1,
|
||||
len(probe_points)))
|
||||
|
||||
# horizontal_move_z (to prevent probe trigger or hitting bed)
|
||||
self._move_helper((None, None, self.horizontal_move_z))
|
||||
|
||||
# move to point to probe
|
||||
self._move_helper((probe_points[self.current_point_index][0],
|
||||
probe_points[self.current_point_index][1], None))
|
||||
|
||||
# probe the point
|
||||
self.current_measured_z = self.probe.run_probe(self.gcmd)[2]
|
||||
|
||||
# horizontal_move_z (to prevent probe trigger or hitting bed)
|
||||
self._move_helper((None, None, self.horizontal_move_z))
|
||||
|
||||
# move the nozzle over the probe point
|
||||
self._move_helper((nozzle_points[self.current_point_index]))
|
||||
|
||||
# start the manual (nozzle) probe
|
||||
ManualProbe.ManualProbeHelper(
|
||||
self.printer, self.gcmd,
|
||||
self._manual_probe_callback_factory(
|
||||
probe_points, nozzle_points, interval))
|
||||
|
||||
def _manual_probe_callback_factory(self, probe_points,
|
||||
nozzle_points, interval):
|
||||
# returns a callback function for the manual probe
|
||||
is_end = self.current_point_index == len(probe_points) - 1
|
||||
|
||||
def callback(kin_pos):
|
||||
if kin_pos is None:
|
||||
# probe was cancelled
|
||||
self.gcmd.respond_info(
|
||||
"AXIS_TWIST_COMPENSATION_CALIBRATE: Probe cancelled, "
|
||||
"calibration aborted")
|
||||
return
|
||||
z_offset = self.current_measured_z - kin_pos[2]
|
||||
self.results.append(z_offset)
|
||||
if is_end:
|
||||
# end of calibration
|
||||
self._finalize_calibration()
|
||||
else:
|
||||
# move to next point
|
||||
self.current_point_index += 1
|
||||
self._calibration(probe_points, nozzle_points, interval)
|
||||
return callback
|
||||
|
||||
def _finalize_calibration(self):
|
||||
# finalize the calibration process
|
||||
# calculate average of results
|
||||
avg = sum(self.results) / len(self.results)
|
||||
# subtract average from each result
|
||||
# so that they are independent of z_offset
|
||||
self.results = [avg - x for x in self.results]
|
||||
# save the config
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
values_as_str = ', '.join(["{:.6f}".format(x)
|
||||
for x in self.results])
|
||||
configfile.set(self.configname, 'z_compensations', values_as_str)
|
||||
configfile.set(self.configname, 'compensation_start_x',
|
||||
self.start_point[0])
|
||||
configfile.set(self.configname, 'compensation_end_x',
|
||||
self.end_point[0])
|
||||
self.compensation.z_compensations = self.results
|
||||
self.compensation.compensation_start_x = self.start_point[0]
|
||||
self.compensation.compensation_end_x = self.end_point[0]
|
||||
self.gcode.respond_info(
|
||||
"AXIS_TWIST_COMPENSATION state has been saved "
|
||||
"for the current session. The SAVE_CONFIG command will "
|
||||
"update the printer config file and restart the printer.")
|
||||
# output result
|
||||
self.gcmd.respond_info(
|
||||
"AXIS_TWIST_COMPENSATION_CALIBRATE: Calibration complete, "
|
||||
"offsets: %s, mean z_offset: %f"
|
||||
% (self.results, avg))
|
||||
|
||||
|
||||
# klipper's entry point using [axis_twist_compensation] section in printer.cfg
|
||||
def load_config(config):
|
||||
return AxisTwistCompensation(config)
|
||||
@@ -1,6 +1,5 @@
|
||||
# Mesh Bed Leveling
|
||||
#
|
||||
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2018-2019 Eric Callahan <arksine.code@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
@@ -103,6 +102,7 @@ class BedMesh:
|
||||
self.log_fade_complete = False
|
||||
self.base_fade_target = config.getfloat('fade_target', None)
|
||||
self.fade_target = 0.
|
||||
self.tool_offset = 0.
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.splitter = MoveSplitter(config, self.gcode)
|
||||
# setup persistent storage
|
||||
@@ -129,12 +129,11 @@ class BedMesh:
|
||||
def handle_connect(self):
|
||||
self.toolhead = self.printer.lookup_object('toolhead')
|
||||
self.bmc.print_generated_points(logging.info)
|
||||
self.pmgr.initialize()
|
||||
def set_mesh(self, mesh):
|
||||
if mesh is not None and self.fade_end != self.FADE_DISABLE:
|
||||
self.log_fade_complete = True
|
||||
if self.base_fade_target is None:
|
||||
self.fade_target = mesh.avg_z
|
||||
self.fade_target = mesh.get_z_average()
|
||||
else:
|
||||
self.fade_target = self.base_fade_target
|
||||
min_z, max_z = mesh.get_z_range()
|
||||
@@ -159,6 +158,7 @@ class BedMesh:
|
||||
"mesh max: %.4f" % (self.fade_dist, min_z, max_z))
|
||||
else:
|
||||
self.fade_target = 0.
|
||||
self.tool_offset = 0.
|
||||
self.z_mesh = mesh
|
||||
self.splitter.initialize(mesh, self.fade_target)
|
||||
# cache the current position before a transform takes place
|
||||
@@ -166,6 +166,7 @@ class BedMesh:
|
||||
gcode_move.reset_last_position()
|
||||
self.update_status()
|
||||
def get_z_factor(self, z_pos):
|
||||
z_pos += self.tool_offset
|
||||
if z_pos >= self.fade_end:
|
||||
return 0.
|
||||
elif z_pos >= self.fade_start:
|
||||
@@ -184,14 +185,15 @@ class BedMesh:
|
||||
max_adj = self.z_mesh.calc_z(x, y)
|
||||
factor = 1.
|
||||
z_adj = max_adj - self.fade_target
|
||||
if min(z, (z - max_adj)) >= self.fade_end:
|
||||
fade_z_pos = z + self.tool_offset
|
||||
if min(fade_z_pos, (fade_z_pos - max_adj)) >= self.fade_end:
|
||||
# Fade out is complete, no factor
|
||||
factor = 0.
|
||||
elif max(z, (z - max_adj)) >= self.fade_start:
|
||||
elif max(fade_z_pos, (fade_z_pos - max_adj)) >= self.fade_start:
|
||||
# Likely in the process of fading out adjustment.
|
||||
# Because we don't yet know the gcode z position, use
|
||||
# algebra to calculate the factor from the toolhead pos
|
||||
factor = ((self.fade_end + self.fade_target - z) /
|
||||
factor = ((self.fade_end + self.fade_target - fade_z_pos) /
|
||||
(self.fade_dist - z_adj))
|
||||
factor = constrain(factor, 0., 1.)
|
||||
final_z_adj = factor * z_adj + self.fade_target
|
||||
@@ -235,7 +237,7 @@ class BedMesh:
|
||||
mesh_max = (params['max_x'], params['max_y'])
|
||||
probed_matrix = self.z_mesh.get_probed_matrix()
|
||||
mesh_matrix = self.z_mesh.get_mesh_matrix()
|
||||
self.status['profile_name'] = self.pmgr.get_current_profile()
|
||||
self.status['profile_name'] = self.z_mesh.get_profile_name()
|
||||
self.status['mesh_min'] = mesh_min
|
||||
self.status['mesh_max'] = mesh_max
|
||||
self.status['probed_matrix'] = probed_matrix
|
||||
@@ -273,12 +275,21 @@ class BedMesh:
|
||||
for i, axis in enumerate(['X', 'Y']):
|
||||
offsets[i] = gcmd.get_float(axis, None)
|
||||
self.z_mesh.set_mesh_offsets(offsets)
|
||||
tool_offset = gcmd.get_float("ZFADE", None)
|
||||
if tool_offset is not None:
|
||||
self.tool_offset = tool_offset
|
||||
gcode_move = self.printer.lookup_object('gcode_move')
|
||||
gcode_move.reset_last_position()
|
||||
else:
|
||||
gcmd.respond_info("No mesh loaded to offset")
|
||||
|
||||
|
||||
class ZrefMode:
|
||||
DISABLED = 0 # Zero reference disabled
|
||||
IN_MESH = 1 # Zero reference position within mesh
|
||||
PROBE = 2 # Zero refrennce position outside of mesh, probe needed
|
||||
|
||||
|
||||
class BedMeshCalibrate:
|
||||
ALGOS = ['lagrange', 'bicubic']
|
||||
def __init__(self, config, bedmesh):
|
||||
@@ -286,17 +297,18 @@ class BedMeshCalibrate:
|
||||
self.orig_config = {'radius': None, 'origin': None}
|
||||
self.radius = self.origin = None
|
||||
self.mesh_min = self.mesh_max = (0., 0.)
|
||||
self.relative_reference_index = config.getint(
|
||||
'relative_reference_index', None)
|
||||
self.adaptive_margin = config.getfloat('adaptive_margin', 0.0)
|
||||
self.zero_ref_pos = config.getfloatlist(
|
||||
"zero_reference_position", None, count=2
|
||||
)
|
||||
self.zero_reference_mode = ZrefMode.DISABLED
|
||||
self.faulty_regions = []
|
||||
self.substituted_indices = collections.OrderedDict()
|
||||
self.orig_config['rri'] = self.relative_reference_index
|
||||
self.bedmesh = bedmesh
|
||||
self.mesh_config = collections.OrderedDict()
|
||||
self._init_mesh_config(config)
|
||||
self._generate_points(config.error)
|
||||
self._profile_name = None
|
||||
self.orig_points = self.points
|
||||
self._profile_name = "default"
|
||||
self.probe_helper = probe.ProbePointsHelper(
|
||||
config, self.probe_finalize, self._get_adjusted_points())
|
||||
self.probe_helper.minimum_points(3)
|
||||
@@ -305,7 +317,13 @@ class BedMeshCalibrate:
|
||||
self.gcode.register_command(
|
||||
'BED_MESH_CALIBRATE', self.cmd_BED_MESH_CALIBRATE,
|
||||
desc=self.cmd_BED_MESH_CALIBRATE_help)
|
||||
def _generate_points(self, error):
|
||||
# Save z offset temporarily and apply to bed mesh
|
||||
self.probed_z_offset = 0
|
||||
self.apply_to_bed_mesh = False
|
||||
self.gcode.register_command(
|
||||
'SAVE_Z_OFFSET_TO_BED_MESH',
|
||||
self.cmd_SAVE_Z_OFFSET_TO_BED_MESH)
|
||||
def _generate_points(self, error, probe_method="automatic"):
|
||||
x_cnt = self.mesh_config['x_count']
|
||||
y_cnt = self.mesh_config['y_count']
|
||||
min_x, min_y = self.mesh_min
|
||||
@@ -315,7 +333,7 @@ class BedMeshCalibrate:
|
||||
# floor distances down to next hundredth
|
||||
x_dist = math.floor(x_dist * 100) / 100
|
||||
y_dist = math.floor(y_dist * 100) / 100
|
||||
if x_dist <= 1. or y_dist <= 1.:
|
||||
if x_dist < 1. or y_dist < 1.:
|
||||
raise error("bed_mesh: min/max points too close together")
|
||||
|
||||
if self.radius is not None:
|
||||
@@ -348,9 +366,32 @@ class BedMeshCalibrate:
|
||||
(self.origin[0] + pos_x, self.origin[1] + pos_y))
|
||||
pos_y += y_dist
|
||||
self.points = points
|
||||
if self.zero_ref_pos is None or probe_method == "manual":
|
||||
# Zero Reference Disabled
|
||||
self.zero_reference_mode = ZrefMode.DISABLED
|
||||
elif within(self.zero_ref_pos, self.mesh_min, self.mesh_max):
|
||||
# Zero Reference position within mesh
|
||||
self.zero_reference_mode = ZrefMode.IN_MESH
|
||||
else:
|
||||
# Zero Reference position outside of mesh
|
||||
self.zero_reference_mode = ZrefMode.PROBE
|
||||
if not self.faulty_regions:
|
||||
return
|
||||
self.substituted_indices.clear()
|
||||
if self.zero_reference_mode == ZrefMode.PROBE:
|
||||
# Cannot probe a reference within a faulty region
|
||||
for min_c, max_c in self.faulty_regions:
|
||||
if within(self.zero_ref_pos, min_c, max_c):
|
||||
opt = "zero_reference_position"
|
||||
raise error(
|
||||
"bed_mesh: Cannot probe zero reference position at "
|
||||
"(%.2f, %.2f) as it is located within a faulty region."
|
||||
" Check the value for option '%s'"
|
||||
% (self.zero_ref_pos[0], self.zero_ref_pos[1], opt,)
|
||||
)
|
||||
# Check to see if any points fall within faulty regions
|
||||
if probe_method == "manual":
|
||||
return
|
||||
last_y = self.points[0][1]
|
||||
is_reversed = False
|
||||
for i, coord in enumerate(self.points):
|
||||
@@ -399,11 +440,11 @@ class BedMeshCalibrate:
|
||||
mesh_pt = "(%.1f, %.1f)" % (x, y)
|
||||
print_func(
|
||||
" %-4d| %-16s| %s" % (i, adj_pt, mesh_pt))
|
||||
if self.relative_reference_index is not None:
|
||||
rri = self.relative_reference_index
|
||||
if self.zero_ref_pos is not None:
|
||||
print_func(
|
||||
"bed_mesh: relative_reference_index %d is (%.2f, %.2f)"
|
||||
% (rri, self.points[rri][0], self.points[rri][1]))
|
||||
"bed_mesh: zero_reference_position is (%.2f, %.2f)"
|
||||
% (self.zero_ref_pos[0], self.zero_ref_pos[1])
|
||||
)
|
||||
if self.substituted_indices:
|
||||
print_func("bed_mesh: faulty region points")
|
||||
for i, v in self.substituted_indices.items():
|
||||
@@ -519,11 +560,117 @@ class BedMeshCalibrate:
|
||||
"interpolation. Configured Probe Count: %d, %d" %
|
||||
(self.mesh_config['x_count'], self.mesh_config['y_count']))
|
||||
params['algo'] = 'lagrange'
|
||||
def set_adaptive_mesh(self, gcmd):
|
||||
if not gcmd.get_int('ADAPTIVE', 0):
|
||||
return False
|
||||
exclude_objects = self.printer.lookup_object("exclude_object", None)
|
||||
if exclude_objects is None:
|
||||
gcmd.respond_info("Exclude objects not enabled. Using full mesh...")
|
||||
return False
|
||||
objects = exclude_objects.get_status().get("objects", [])
|
||||
if not objects:
|
||||
return False
|
||||
margin = gcmd.get_float('ADAPTIVE_MARGIN', self.adaptive_margin)
|
||||
|
||||
# List all exclude_object points by axis and iterate over
|
||||
# all polygon points, and pick the min and max or each axis
|
||||
list_of_xs = []
|
||||
list_of_ys = []
|
||||
gcmd.respond_info("Found %s objects" % (len(objects)))
|
||||
for obj in objects:
|
||||
for point in obj["polygon"]:
|
||||
list_of_xs.append(point[0])
|
||||
list_of_ys.append(point[1])
|
||||
|
||||
# Define bounds of adaptive mesh area
|
||||
mesh_min = [min(list_of_xs), min(list_of_ys)]
|
||||
mesh_max = [max(list_of_xs), max(list_of_ys)]
|
||||
adjusted_mesh_min = [x - margin for x in mesh_min]
|
||||
adjusted_mesh_max = [x + margin for x in mesh_max]
|
||||
|
||||
# Force margin to respect original mesh bounds
|
||||
adjusted_mesh_min[0] = max(adjusted_mesh_min[0],
|
||||
self.orig_config["mesh_min"][0])
|
||||
adjusted_mesh_min[1] = max(adjusted_mesh_min[1],
|
||||
self.orig_config["mesh_min"][1])
|
||||
adjusted_mesh_max[0] = min(adjusted_mesh_max[0],
|
||||
self.orig_config["mesh_max"][0])
|
||||
adjusted_mesh_max[1] = min(adjusted_mesh_max[1],
|
||||
self.orig_config["mesh_max"][1])
|
||||
|
||||
adjusted_mesh_size = (adjusted_mesh_max[0] - adjusted_mesh_min[0],
|
||||
adjusted_mesh_max[1] - adjusted_mesh_min[1])
|
||||
|
||||
# Compute a ratio between the adapted and original sizes
|
||||
ratio = (adjusted_mesh_size[0] /
|
||||
(self.orig_config["mesh_max"][0] -
|
||||
self.orig_config["mesh_min"][0]),
|
||||
adjusted_mesh_size[1] /
|
||||
(self.orig_config["mesh_max"][1] -
|
||||
self.orig_config["mesh_min"][1]))
|
||||
|
||||
gcmd.respond_info("Original mesh bounds: (%s,%s)" %
|
||||
(self.orig_config["mesh_min"],
|
||||
self.orig_config["mesh_max"]))
|
||||
gcmd.respond_info("Original probe count: (%s,%s)" %
|
||||
(self.mesh_config["x_count"],
|
||||
self.mesh_config["y_count"]))
|
||||
gcmd.respond_info("Adapted mesh bounds: (%s,%s)" %
|
||||
(adjusted_mesh_min, adjusted_mesh_max))
|
||||
gcmd.respond_info("Ratio: (%s, %s)" % ratio)
|
||||
|
||||
new_x_probe_count = int(
|
||||
math.ceil(self.mesh_config["x_count"] * ratio[0]))
|
||||
new_y_probe_count = int(
|
||||
math.ceil(self.mesh_config["y_count"] * ratio[1]))
|
||||
|
||||
# There is one case, where we may have to adjust the probe counts:
|
||||
# axis0 < 4 and axis1 > 6 (see _verify_algorithm).
|
||||
min_num_of_probes = 3
|
||||
if max(new_x_probe_count, new_y_probe_count) > 6 and \
|
||||
min(new_x_probe_count, new_y_probe_count) < 4:
|
||||
min_num_of_probes = 4
|
||||
|
||||
new_x_probe_count = max(min_num_of_probes, new_x_probe_count)
|
||||
new_y_probe_count = max(min_num_of_probes, new_y_probe_count)
|
||||
|
||||
gcmd.respond_info("Adapted probe count: (%s,%s)" %
|
||||
(new_x_probe_count, new_y_probe_count))
|
||||
|
||||
# If the adapted mesh size is too small, adjust it to something
|
||||
# useful.
|
||||
adjusted_mesh_size = (max(adjusted_mesh_size[0], new_x_probe_count),
|
||||
max(adjusted_mesh_size[1], new_y_probe_count))
|
||||
|
||||
if self.radius is not None:
|
||||
adapted_radius = math.sqrt((adjusted_mesh_size[0] ** 2) +
|
||||
(adjusted_mesh_size[1] ** 2)) / 2
|
||||
adapted_origin = (adjusted_mesh_min[0] +
|
||||
(adjusted_mesh_size[0] / 2),
|
||||
adjusted_mesh_min[1] +
|
||||
(adjusted_mesh_size[1] / 2))
|
||||
to_adapted_origin = math.sqrt(adapted_origin[0]**2 +
|
||||
adapted_origin[1]**2)
|
||||
# If the adapted mesh size is smaller than the default/full
|
||||
# mesh, adjust the parameters. Otherwise, just do the full mesh.
|
||||
if adapted_radius + to_adapted_origin < self.radius:
|
||||
self.radius = adapted_radius
|
||||
self.origin = adapted_origin
|
||||
self.mesh_min = (-self.radius, -self.radius)
|
||||
self.mesh_max = (self.radius, self.radius)
|
||||
self.mesh_config["x_count"] = self.mesh_config["y_count"] = \
|
||||
max(new_x_probe_count, new_y_probe_count)
|
||||
else:
|
||||
self.mesh_min = adjusted_mesh_min
|
||||
self.mesh_max = adjusted_mesh_max
|
||||
self.mesh_config["x_count"] = new_x_probe_count
|
||||
self.mesh_config["y_count"] = new_y_probe_count
|
||||
self._profile_name = None
|
||||
return True
|
||||
def update_config(self, gcmd):
|
||||
# reset default configuration
|
||||
self.radius = self.orig_config['radius']
|
||||
self.origin = self.orig_config['origin']
|
||||
self.relative_reference_index = self.orig_config['rri']
|
||||
self.mesh_min = self.orig_config['mesh_min']
|
||||
self.mesh_max = self.orig_config['mesh_max']
|
||||
for key in list(self.mesh_config.keys()):
|
||||
@@ -531,12 +678,6 @@ class BedMeshCalibrate:
|
||||
|
||||
params = gcmd.get_command_parameters()
|
||||
need_cfg_update = False
|
||||
if 'RELATIVE_REFERENCE_INDEX' in params:
|
||||
self.relative_reference_index = gcmd.get_int(
|
||||
'RELATIVE_REFERENCE_INDEX')
|
||||
if self.relative_reference_index < 0:
|
||||
self.relative_reference_index = None
|
||||
need_cfg_update = True
|
||||
if self.radius is not None:
|
||||
if "MESH_RADIUS" in params:
|
||||
self.radius = gcmd.get_float("MESH_RADIUS")
|
||||
@@ -569,45 +710,64 @@ class BedMeshCalibrate:
|
||||
self.mesh_config['algo'] = gcmd.get('ALGORITHM').strip().lower()
|
||||
need_cfg_update = True
|
||||
|
||||
need_cfg_update |= self.set_adaptive_mesh(gcmd)
|
||||
probe_method = gcmd.get("METHOD", "automatic")
|
||||
|
||||
if need_cfg_update:
|
||||
self._verify_algorithm(gcmd.error)
|
||||
self._generate_points(gcmd.error)
|
||||
self._generate_points(gcmd.error, probe_method)
|
||||
gcmd.respond_info("Generating new points...")
|
||||
self.print_generated_points(gcmd.respond_info)
|
||||
pts = self._get_adjusted_points()
|
||||
self.probe_helper.update_probe_points(pts, 3)
|
||||
msg = "relative_reference_index: %s\n" % \
|
||||
(self.relative_reference_index)
|
||||
msg += "\n".join(["%s: %s" % (k, v) for k, v
|
||||
in self.mesh_config.items()])
|
||||
msg = "\n".join(["%s: %s" % (k, v)
|
||||
for k, v in self.mesh_config.items()])
|
||||
logging.info("Updated Mesh Configuration:\n" + msg)
|
||||
else:
|
||||
self.points = self.orig_points
|
||||
self._generate_points(gcmd.error, probe_method)
|
||||
pts = self._get_adjusted_points()
|
||||
self.probe_helper.update_probe_points(pts, 3)
|
||||
def _get_adjusted_points(self):
|
||||
if not self.substituted_indices:
|
||||
return self.points
|
||||
adj_pts = []
|
||||
last_index = 0
|
||||
for i, pts in self.substituted_indices.items():
|
||||
adj_pts.extend(self.points[last_index:i])
|
||||
adj_pts.extend(pts)
|
||||
# Add one to the last index to skip the point
|
||||
# we are replacing
|
||||
last_index = i + 1
|
||||
adj_pts.extend(self.points[last_index:])
|
||||
if self.substituted_indices:
|
||||
last_index = 0
|
||||
for i, pts in self.substituted_indices.items():
|
||||
adj_pts.extend(self.points[last_index:i])
|
||||
adj_pts.extend(pts)
|
||||
# Add one to the last index to skip the point
|
||||
# we are replacing
|
||||
last_index = i + 1
|
||||
adj_pts.extend(self.points[last_index:])
|
||||
else:
|
||||
adj_pts = list(self.points)
|
||||
if self.zero_reference_mode == ZrefMode.PROBE:
|
||||
adj_pts.append(self.zero_ref_pos)
|
||||
return adj_pts
|
||||
cmd_BED_MESH_CALIBRATE_help = "Perform Mesh Bed Leveling"
|
||||
def cmd_BED_MESH_CALIBRATE(self, gcmd):
|
||||
self._profile_name = gcmd.get('PROFILE', "default")
|
||||
if not self._profile_name.strip():
|
||||
raise gcmd.error("Value for parameter 'PROFILE' must be specified")
|
||||
self.bedmesh.set_mesh(None)
|
||||
self.update_config(gcmd)
|
||||
self.probe_helper.start_probe(gcmd)
|
||||
|
||||
def cmd_SAVE_Z_OFFSET_TO_BED_MESH(self, gcmd):
|
||||
self.probed_z_offset = self.printer.lookup_object('probe').last_z_result
|
||||
self.apply_to_bed_mesh = gcmd.get('APPLY', True)
|
||||
|
||||
def probe_finalize(self, offsets, positions):
|
||||
x_offset, y_offset, z_offset = offsets
|
||||
positions = [[round(p[0], 2), round(p[1], 2), p[2]]
|
||||
for p in positions]
|
||||
if self.zero_reference_mode == ZrefMode.PROBE:
|
||||
ref_pos = positions.pop()
|
||||
logging.info(
|
||||
"bed_mesh: z-offset replaced with probed z value at "
|
||||
"position (%.2f, %.2f, %.6f)"
|
||||
% (ref_pos[0], ref_pos[1], ref_pos[2])
|
||||
)
|
||||
z_offset = ref_pos[2]
|
||||
params = dict(self.mesh_config)
|
||||
params['min_x'] = min(positions, key=lambda p: p[0])[0] + x_offset
|
||||
params['max_x'] = max(positions, key=lambda p: p[0])[0] + x_offset
|
||||
@@ -658,11 +818,6 @@ class BedMeshCalibrate:
|
||||
% (off_pt[0], off_pt[1], probed[0], probed[1]))
|
||||
positions = corrected_pts
|
||||
|
||||
if self.relative_reference_index is not None:
|
||||
# zero out probe z offset and
|
||||
# set offset relative to reference index
|
||||
z_offset = positions[self.relative_reference_index][2]
|
||||
|
||||
probed_matrix = []
|
||||
row = []
|
||||
prev_pos = positions[0]
|
||||
@@ -714,14 +869,25 @@ class BedMeshCalibrate:
|
||||
"Probed table length: %d Probed Table:\n%s") %
|
||||
(len(probed_matrix), str(probed_matrix)))
|
||||
|
||||
z_mesh = ZMesh(params)
|
||||
z_mesh = ZMesh(params, self._profile_name)
|
||||
try:
|
||||
if self.apply_to_bed_mesh:
|
||||
for row in range(len(probed_matrix)):
|
||||
for col in range(len(probed_matrix[row])):
|
||||
probed_matrix[row][col] -= self.probed_z_offset
|
||||
self.apply_to_bed_mesh = False
|
||||
z_mesh.build_mesh(probed_matrix)
|
||||
except BedMeshError as e:
|
||||
raise self.gcode.error(str(e))
|
||||
if self.zero_reference_mode == ZrefMode.IN_MESH:
|
||||
# The reference can be anywhere in the mesh, therefore
|
||||
# it is necessary to set the reference after the initial mesh
|
||||
# is generated to lookup the correct z value.
|
||||
z_mesh.set_zero_reference(*self.zero_ref_pos)
|
||||
self.bedmesh.set_mesh(z_mesh)
|
||||
self.gcode.respond_info("Mesh Bed Leveling Complete")
|
||||
self.bedmesh.save_profile(self._profile_name)
|
||||
if self._profile_name is not None:
|
||||
self.bedmesh.save_profile(self._profile_name)
|
||||
def _dump_points(self, probed_pts, corrected_pts, offsets):
|
||||
# logs generated points with offset applied, points received
|
||||
# from the finalize callback, and the list of corrected points
|
||||
@@ -807,10 +973,10 @@ class MoveSplitter:
|
||||
|
||||
|
||||
class ZMesh:
|
||||
def __init__(self, params):
|
||||
def __init__(self, params, name):
|
||||
self.profile_name = name or "adaptive-%X" % (id(self),)
|
||||
self.probed_matrix = self.mesh_matrix = None
|
||||
self.mesh_params = params
|
||||
self.avg_z = 0.
|
||||
self.mesh_offsets = [0., 0.]
|
||||
logging.debug('bed_mesh: probe/mesh parameters:')
|
||||
for key, value in self.mesh_params.items():
|
||||
@@ -857,6 +1023,8 @@ class ZMesh:
|
||||
return [[]]
|
||||
def get_mesh_params(self):
|
||||
return self.mesh_params
|
||||
def get_profile_name(self):
|
||||
return self.profile_name
|
||||
def print_probed_matrix(self, print_func):
|
||||
if self.probed_matrix is not None:
|
||||
msg = "Mesh Leveling Probed Z positions:\n"
|
||||
@@ -875,7 +1043,7 @@ class ZMesh:
|
||||
msg += "Search Height: %d\n" % (move_z)
|
||||
msg += "Mesh Offsets: X=%.4f, Y=%.4f\n" % (
|
||||
self.mesh_offsets[0], self.mesh_offsets[1])
|
||||
msg += "Mesh Average: %.2f\n" % (self.avg_z)
|
||||
msg += "Mesh Average: %.2f\n" % (self.get_z_average())
|
||||
rng = self.get_z_range()
|
||||
msg += "Mesh Range: min=%.4f max=%.4f\n" % (rng[0], rng[1])
|
||||
msg += "Interpolation Algorithm: %s\n" \
|
||||
@@ -891,13 +1059,17 @@ class ZMesh:
|
||||
def build_mesh(self, z_matrix):
|
||||
self.probed_matrix = z_matrix
|
||||
self._sample(z_matrix)
|
||||
self.avg_z = (sum([sum(x) for x in self.mesh_matrix]) /
|
||||
sum([len(x) for x in self.mesh_matrix]))
|
||||
# Round average to the nearest 100th. This
|
||||
# should produce an offset that is divisible by common
|
||||
# z step distances
|
||||
self.avg_z = round(self.avg_z, 2)
|
||||
self.print_mesh(logging.debug)
|
||||
def set_zero_reference(self, xpos, ypos):
|
||||
offset = self.calc_z(xpos, ypos)
|
||||
logging.info(
|
||||
"bed_mesh: setting zero reference at (%.2f, %.2f, %.6f)"
|
||||
% (xpos, ypos, offset)
|
||||
)
|
||||
for matrix in [self.probed_matrix, self.mesh_matrix]:
|
||||
for yidx in range(len(matrix)):
|
||||
for xidx in range(len(matrix[yidx])):
|
||||
matrix[yidx][xidx] -= offset
|
||||
def set_mesh_offsets(self, offsets):
|
||||
for i, o in enumerate(offsets):
|
||||
if o is not None:
|
||||
@@ -924,6 +1096,16 @@ class ZMesh:
|
||||
return mesh_min, mesh_max
|
||||
else:
|
||||
return 0., 0.
|
||||
def get_z_average(self):
|
||||
if self.mesh_matrix is not None:
|
||||
avg_z = (sum([sum(x) for x in self.mesh_matrix]) /
|
||||
sum([len(x) for x in self.mesh_matrix]))
|
||||
# Round average to the nearest 100th. This
|
||||
# should produce an offset that is divisible by common
|
||||
# z step distances
|
||||
return round(avg_z, 2)
|
||||
else:
|
||||
return 0.
|
||||
def _get_linear_index(self, coord, axis):
|
||||
if axis == 0:
|
||||
# X-axis
|
||||
@@ -1103,7 +1285,6 @@ class ProfileManager:
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
self.bedmesh = bedmesh
|
||||
self.profiles = {}
|
||||
self.current_profile = ""
|
||||
self.incompatible_profiles = []
|
||||
# Fetch stored profiles from Config
|
||||
stored_profs = config.get_prefix_sections(self.name)
|
||||
@@ -1135,14 +1316,8 @@ class ProfileManager:
|
||||
self.gcode.register_command(
|
||||
'BED_MESH_PROFILE', self.cmd_BED_MESH_PROFILE,
|
||||
desc=self.cmd_BED_MESH_PROFILE_help)
|
||||
def initialize(self):
|
||||
self._check_incompatible_profiles()
|
||||
if "default" in self.profiles:
|
||||
self.load_profile("default")
|
||||
def get_profiles(self):
|
||||
return self.profiles
|
||||
def get_current_profile(self):
|
||||
return self.current_profile
|
||||
def _check_incompatible_profiles(self):
|
||||
if self.incompatible_profiles:
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
@@ -1183,7 +1358,6 @@ class ProfileManager:
|
||||
profile['points'] = probed_matrix
|
||||
profile['mesh_params'] = collections.OrderedDict(mesh_params)
|
||||
self.profiles = profiles
|
||||
self.current_profile = prof_name
|
||||
self.bedmesh.update_status()
|
||||
self.gcode.respond_info(
|
||||
"Bed Mesh state has been saved to profile [%s]\n"
|
||||
@@ -1197,12 +1371,11 @@ class ProfileManager:
|
||||
"bed_mesh: Unknown profile [%s]" % prof_name)
|
||||
probed_matrix = profile['points']
|
||||
mesh_params = profile['mesh_params']
|
||||
z_mesh = ZMesh(mesh_params)
|
||||
z_mesh = ZMesh(mesh_params, prof_name)
|
||||
try:
|
||||
z_mesh.build_mesh(probed_matrix)
|
||||
except BedMeshError as e:
|
||||
raise self.gcode.error(str(e))
|
||||
self.current_profile = prof_name
|
||||
self.bedmesh.set_mesh(z_mesh)
|
||||
def remove_profile(self, prof_name):
|
||||
if prof_name in self.profiles:
|
||||
@@ -1229,6 +1402,10 @@ class ProfileManager:
|
||||
for key in options:
|
||||
name = gcmd.get(key, None)
|
||||
if name is not None:
|
||||
if not name.strip():
|
||||
raise gcmd.error(
|
||||
"Value for parameter '%s' must be specified" % (key)
|
||||
)
|
||||
if name == "default" and key == 'SAVE':
|
||||
gcmd.respond_info(
|
||||
"Profile 'default' is reserved, please choose"
|
||||
|
||||
Binary file not shown.
@@ -7,9 +7,7 @@
|
||||
class BedScrews:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.state = None
|
||||
self.current_screw = 0
|
||||
self.accepted_screws = 0
|
||||
self.reset()
|
||||
self.number_of_screws = 0
|
||||
# Read config
|
||||
screws = []
|
||||
@@ -39,8 +37,17 @@ class BedScrews:
|
||||
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)
|
||||
try:
|
||||
self.printer.lookup_object('toolhead').manual_move(coord, speed)
|
||||
except self.printer.command_error as e:
|
||||
self.unregister_commands()
|
||||
self.reset()
|
||||
raise
|
||||
def move_to_screw(self, state, screw):
|
||||
# Move up, over, and then down
|
||||
self.move((None, None, self.horizontal_move_z), self.lift_speed)
|
||||
@@ -64,6 +71,13 @@ class BedScrews:
|
||||
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:
|
||||
@@ -92,7 +106,7 @@ class BedScrews:
|
||||
self.move_to_screw('fine', 0)
|
||||
return
|
||||
# Done
|
||||
self.state = None
|
||||
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"
|
||||
@@ -103,7 +117,7 @@ class BedScrews:
|
||||
cmd_ABORT_help = "Abort bed screws tool"
|
||||
def cmd_ABORT(self, gcmd):
|
||||
self.unregister_commands()
|
||||
self.state = None
|
||||
self.reset()
|
||||
|
||||
def load_config(config):
|
||||
return BedScrews(config)
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,7 @@ from . import bus
|
||||
|
||||
REPORT_TIME = .8
|
||||
BME280_CHIP_ADDR = 0x76
|
||||
|
||||
BME280_REGS = {
|
||||
'RESET': 0xE0, 'CTRL_HUM': 0xF2,
|
||||
'STATUS': 0xF3, 'CTRL_MEAS': 0xF4, 'CONFIG': 0xF5,
|
||||
@@ -46,6 +47,16 @@ BME680_GAS_CONSTANTS = {
|
||||
15: (1., 244.140625)
|
||||
}
|
||||
|
||||
BMP180_REGS = {
|
||||
'RESET': 0xE0,
|
||||
'CAL_1': 0xAA,
|
||||
'CTRL_MEAS': 0xF4,
|
||||
'REG_MSB': 0xF6,
|
||||
'REG_LSB': 0xF7,
|
||||
'CRV_TEMP': 0x2E,
|
||||
'CRV_PRES': 0x34
|
||||
}
|
||||
|
||||
STATUS_MEASURING = 1 << 3
|
||||
STATUS_IM_UPDATE = 1
|
||||
MODE = 1
|
||||
@@ -57,7 +68,7 @@ MEASURE_DONE = 1 << 5
|
||||
RESET_CHIP_VALUE = 0xB6
|
||||
|
||||
BME_CHIPS = {
|
||||
0x58: 'BMP280', 0x60: 'BME280', 0x61: 'BME680'
|
||||
0x58: 'BMP280', 0x60: 'BME280', 0x61: 'BME680', 0x55: 'BMP180'
|
||||
}
|
||||
BME_CHIP_ID_REG = 0xD0
|
||||
|
||||
@@ -81,6 +92,14 @@ def get_signed_byte(bits):
|
||||
return get_twos_complement(bits, 8)
|
||||
|
||||
|
||||
def get_unsigned_short_msb(bits):
|
||||
return bits[0] << 8 | bits[1]
|
||||
|
||||
|
||||
def get_signed_short_msb(bits):
|
||||
val = get_unsigned_short_msb(bits)
|
||||
return get_twos_complement(val, 16)
|
||||
|
||||
class BME280:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
@@ -188,6 +207,23 @@ class BME280:
|
||||
dig['G3'] = get_signed_byte(calib_data_2[13])
|
||||
return dig
|
||||
|
||||
def read_calibration_data_bmp180(calib_data_1):
|
||||
dig = {}
|
||||
dig['AC1'] = get_signed_short_msb(calib_data_1[0:2])
|
||||
dig['AC2'] = get_signed_short_msb(calib_data_1[2:4])
|
||||
dig['AC3'] = get_signed_short_msb(calib_data_1[4:6])
|
||||
dig['AC4'] = get_unsigned_short_msb(calib_data_1[6:8])
|
||||
dig['AC5'] = get_unsigned_short_msb(calib_data_1[8:10])
|
||||
dig['AC6'] = get_unsigned_short_msb(calib_data_1[10:12])
|
||||
|
||||
dig['B1'] = get_signed_short_msb(calib_data_1[12:14])
|
||||
dig['B2'] = get_signed_short_msb(calib_data_1[14:16])
|
||||
|
||||
dig['MB'] = get_signed_short_msb(calib_data_1[16:18])
|
||||
dig['MC'] = get_signed_short_msb(calib_data_1[18:20])
|
||||
dig['MD'] = get_signed_short_msb(calib_data_1[20:22])
|
||||
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)
|
||||
@@ -201,15 +237,21 @@ class BME280:
|
||||
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)
|
||||
if self.chip_type != 'BMP180':
|
||||
# BMP180 has no status register available
|
||||
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
|
||||
elif self.chip_type == 'BMP180':
|
||||
self.max_sample_time = (1.25 + ((2.3 * self.os_pres) + .575)) / 1000
|
||||
self.sample_timer = self.reactor.register_timer(self._sample_bmp180)
|
||||
self.chip_registers = BMP180_REGS
|
||||
else:
|
||||
self.max_sample_time = \
|
||||
(1.25 + (2.3 * self.os_temp) + ((2.3 * self.os_pres) + .575)
|
||||
@@ -221,14 +263,19 @@ class 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 == 'BMP180':
|
||||
cal_1 = self.read_register('CAL_1', 22)
|
||||
else:
|
||||
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)
|
||||
elif self.chip_type == 'BMP180':
|
||||
self.dig = read_calibration_data_bmp180(cal_1)
|
||||
|
||||
def _sample_bme280(self, eventtime):
|
||||
# Enter forced mode
|
||||
@@ -332,7 +379,44 @@ class BME280:
|
||||
% (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
|
||||
return measured_time + REPORT_TIME
|
||||
|
||||
def _sample_bmp180(self, eventtime):
|
||||
meas = self.chip_registers['CRV_TEMP']
|
||||
self.write_register('CTRL_MEAS', meas)
|
||||
|
||||
try:
|
||||
self.reactor.pause(self.reactor.monotonic() + .01)
|
||||
data = self.read_register('REG_MSB', 2)
|
||||
temp_raw = (data[0] << 8) | data[1]
|
||||
except Exception:
|
||||
logging.exception("BMP180: Error reading temperature")
|
||||
self.temp = self.pressure = .0
|
||||
return self.reactor.NEVER
|
||||
|
||||
meas = self.chip_registers['CRV_PRES'] | (self.os_pres << 6)
|
||||
self.write_register('CTRL_MEAS', meas)
|
||||
|
||||
try:
|
||||
self.reactor.pause(self.reactor.monotonic() + .01)
|
||||
data = self.read_register('REG_MSB', 3)
|
||||
pressure_raw = \
|
||||
((data[0] << 16)|(data[1] << 8)|data[2]) >> (8 - self.os_pres)
|
||||
except Exception:
|
||||
logging.exception("BMP180: Error reading pressure")
|
||||
self.temp = self.pressure = .0
|
||||
return self.reactor.NEVER
|
||||
|
||||
self.temp = self._compensate_temp_bmp180(temp_raw)
|
||||
self.pressure = self._compensate_pressure_bmp180(pressure_raw) / 100.
|
||||
if self.temp < self.min_temp or self.temp > self.max_temp:
|
||||
self.printer.invoke_shutdown(
|
||||
"BMP180 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 _compensate_temp(self, raw_temp):
|
||||
dig = self.dig
|
||||
@@ -443,6 +527,37 @@ class BME280:
|
||||
|
||||
return duration_reg
|
||||
|
||||
def _compensate_temp_bmp180(self, raw_temp):
|
||||
dig = self.dig
|
||||
x1 = (raw_temp - dig['AC6']) * dig['AC5'] / 32768.
|
||||
x2 = dig['MC'] * 2048 / (x1 + dig['MD'])
|
||||
b5 = x1 + x2
|
||||
self.t_fine = b5
|
||||
return (b5 + 8)/16./10.
|
||||
|
||||
def _compensate_pressure_bmp180(self, raw_pressure):
|
||||
dig = self.dig
|
||||
b5 = self.t_fine
|
||||
b6 = b5 - 4000
|
||||
x1 = (dig['B2'] * (b6 * b6 / 4096)) / 2048
|
||||
x2 = dig['AC2'] * b6 / 2048
|
||||
x3 = x1 + x2
|
||||
b3 = ((int(dig['AC1'] * 4 + x3) << self.os_pres) + 2) / 4
|
||||
x1 = dig['AC3'] * b6 / 8192
|
||||
x2 = (dig['B1'] * (b6 * b6 / 4096)) / 65536
|
||||
x3 = ((x1 + x2) + 2) / 4
|
||||
b4 = dig['AC4'] * (x3 + 32768) / 32768
|
||||
b7 = (raw_pressure - b3) * (50000 >> self.os_pres)
|
||||
if (b7 < 0x80000000):
|
||||
p = (b7 * 2) / b4
|
||||
else:
|
||||
p = (b7 / b4) * 2
|
||||
x1 = (p / 256) * (p / 256)
|
||||
x1 = (x1 * 3038) / 65536
|
||||
x2 = (-7357 * p) / 65536
|
||||
p = p + (x1 + x2 + 3791) / 16.
|
||||
return p
|
||||
|
||||
def read_id(self):
|
||||
# read chip id register
|
||||
regs = [BME_CHIP_ID_REG]
|
||||
|
||||
Binary file not shown.
Binary file not shown.
247
klippy/extras/bulk_sensor.py
Normal file
247
klippy/extras/bulk_sensor.py
Normal file
@@ -0,0 +1,247 @@
|
||||
# Tools for reading bulk sensor data from the mcu
|
||||
#
|
||||
# Copyright (C) 2020-2023 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging, threading
|
||||
|
||||
# This "bulk sensor" module facilitates the processing of sensor chip
|
||||
# measurements that do not require the host to respond with low
|
||||
# latency. This module helps collect these measurements into batches
|
||||
# that are then processed periodically by the host code (as specified
|
||||
# by BatchBulkHelper.batch_interval). It supports the collection of
|
||||
# thousands of sensor measurements per second.
|
||||
#
|
||||
# Processing measurements in batches reduces load on the mcu, reduces
|
||||
# bandwidth to/from the mcu, and reduces load on the host. It also
|
||||
# makes it easier to export the raw measurements via the webhooks
|
||||
# system (aka API Server).
|
||||
|
||||
BATCH_INTERVAL = 0.500
|
||||
|
||||
# Helper to process accumulated messages in periodic batches
|
||||
class BatchBulkHelper:
|
||||
def __init__(self, printer, batch_cb, start_cb=None, stop_cb=None,
|
||||
batch_interval=BATCH_INTERVAL):
|
||||
self.printer = printer
|
||||
self.batch_cb = batch_cb
|
||||
if start_cb is None:
|
||||
start_cb = (lambda: None)
|
||||
self.start_cb = start_cb
|
||||
if stop_cb is None:
|
||||
stop_cb = (lambda: None)
|
||||
self.stop_cb = stop_cb
|
||||
self.is_started = False
|
||||
self.batch_interval = batch_interval
|
||||
self.batch_timer = None
|
||||
self.client_cbs = []
|
||||
self.webhooks_start_resp = {}
|
||||
# Periodic batch processing
|
||||
def _start(self):
|
||||
if self.is_started:
|
||||
return
|
||||
self.is_started = True
|
||||
try:
|
||||
self.start_cb()
|
||||
except self.printer.command_error as e:
|
||||
logging.exception("BatchBulkHelper start callback error")
|
||||
self.is_started = False
|
||||
del self.client_cbs[:]
|
||||
raise
|
||||
reactor = self.printer.get_reactor()
|
||||
systime = reactor.monotonic()
|
||||
waketime = systime + self.batch_interval
|
||||
self.batch_timer = reactor.register_timer(self._proc_batch, waketime)
|
||||
def _stop(self):
|
||||
del self.client_cbs[:]
|
||||
self.printer.get_reactor().unregister_timer(self.batch_timer)
|
||||
self.batch_timer = None
|
||||
if not self.is_started:
|
||||
return
|
||||
try:
|
||||
self.stop_cb()
|
||||
except self.printer.command_error as e:
|
||||
logging.exception("BatchBulkHelper stop callback error")
|
||||
del self.client_cbs[:]
|
||||
self.is_started = False
|
||||
if self.client_cbs:
|
||||
# New client started while in process of stopping
|
||||
self._start()
|
||||
def _proc_batch(self, eventtime):
|
||||
try:
|
||||
msg = self.batch_cb(eventtime)
|
||||
except self.printer.command_error as e:
|
||||
logging.exception("BatchBulkHelper batch callback error")
|
||||
self._stop()
|
||||
return self.printer.get_reactor().NEVER
|
||||
if not msg:
|
||||
return eventtime + self.batch_interval
|
||||
for client_cb in list(self.client_cbs):
|
||||
res = client_cb(msg)
|
||||
if not res:
|
||||
# This client no longer needs updates - unregister it
|
||||
self.client_cbs.remove(client_cb)
|
||||
if not self.client_cbs:
|
||||
self._stop()
|
||||
return self.printer.get_reactor().NEVER
|
||||
return eventtime + self.batch_interval
|
||||
# Client registration
|
||||
def add_client(self, client_cb):
|
||||
self.client_cbs.append(client_cb)
|
||||
self._start()
|
||||
# Webhooks registration
|
||||
def _add_api_client(self, web_request):
|
||||
whbatch = BatchWebhooksClient(web_request)
|
||||
self.add_client(whbatch.handle_batch)
|
||||
web_request.send(self.webhooks_start_resp)
|
||||
def add_mux_endpoint(self, path, key, value, webhooks_start_resp):
|
||||
self.webhooks_start_resp = webhooks_start_resp
|
||||
wh = self.printer.lookup_object('webhooks')
|
||||
wh.register_mux_endpoint(path, key, value, self._add_api_client)
|
||||
|
||||
# A webhooks wrapper for use by BatchBulkHelper
|
||||
class BatchWebhooksClient:
|
||||
def __init__(self, web_request):
|
||||
self.cconn = web_request.get_client_connection()
|
||||
self.template = web_request.get_dict('response_template', {})
|
||||
def handle_batch(self, msg):
|
||||
if self.cconn.is_closed():
|
||||
return False
|
||||
tmp = dict(self.template)
|
||||
tmp['params'] = msg
|
||||
self.cconn.send(tmp)
|
||||
return True
|
||||
|
||||
# Helper class to store incoming messages in a queue
|
||||
class BulkDataQueue:
|
||||
def __init__(self, mcu, msg_name, oid):
|
||||
# Measurement storage (accessed from background thread)
|
||||
self.lock = threading.Lock()
|
||||
self.raw_samples = []
|
||||
# Register callback with mcu
|
||||
mcu.register_response(self._handle_data, msg_name, oid)
|
||||
def _handle_data(self, params):
|
||||
with self.lock:
|
||||
self.raw_samples.append(params)
|
||||
def pull_samples(self):
|
||||
with self.lock:
|
||||
raw_samples = self.raw_samples
|
||||
self.raw_samples = []
|
||||
return raw_samples
|
||||
def clear_samples(self):
|
||||
self.pull_samples()
|
||||
|
||||
|
||||
######################################################################
|
||||
# Clock synchronization
|
||||
######################################################################
|
||||
|
||||
# It is common for sensors to produce measurements at a fixed
|
||||
# frequency. If the mcu can reliably obtain all of these
|
||||
# measurements, then the code here can calculate a precision timestamp
|
||||
# for them. That is, it can determine the actual sensor measurement
|
||||
# frequency, the time of the first measurement, and thus a precise
|
||||
# time for all measurements.
|
||||
#
|
||||
# This system works by having the mcu periodically report a precision
|
||||
# timestamp along with the total number of measurements the sensor has
|
||||
# taken as of that time. In brief, knowing the total number of
|
||||
# measurements taken over an extended period provides an accurate
|
||||
# estimate of measurement frequency, which can then also be utilized
|
||||
# to determine the time of the first measurement.
|
||||
|
||||
# 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
|
||||
|
||||
MAX_BULK_MSG_SIZE = 52
|
||||
|
||||
# Handle common periodic chip status query responses
|
||||
class ChipClockUpdater:
|
||||
def __init__(self, clock_sync, bytes_per_sample):
|
||||
self.clock_sync = clock_sync
|
||||
self.bytes_per_sample = bytes_per_sample
|
||||
self.samples_per_block = MAX_BULK_MSG_SIZE // bytes_per_sample
|
||||
self.mcu = clock_sync.mcu
|
||||
self.last_sequence = self.max_query_duration = 0
|
||||
self.last_limit_count = 0
|
||||
def get_last_sequence(self):
|
||||
return self.last_sequence
|
||||
def get_last_limit_count(self):
|
||||
return self.last_limit_count
|
||||
def clear_duration_filter(self):
|
||||
self.max_query_duration = 1 << 31
|
||||
def note_start(self, reqclock):
|
||||
self.last_sequence = 0
|
||||
self.last_limit_count = 0
|
||||
self.clock_sync.reset(reqclock, 0)
|
||||
self.clear_duration_filter()
|
||||
def update_clock(self, params):
|
||||
# Handle a status response message of the form:
|
||||
# adxl345_status oid=x clock=x query_ticks=x next_sequence=x
|
||||
# buffered=x fifo=x limit_count=x
|
||||
fifo = params['fifo']
|
||||
mcu_clock = self.mcu.clock32_to_clock64(params['clock'])
|
||||
seq_diff = (params['next_sequence'] - self.last_sequence) & 0xffff
|
||||
self.last_sequence += seq_diff
|
||||
buffered = params['buffered']
|
||||
lc_diff = (params['limit_count'] - self.last_limit_count) & 0xffff
|
||||
self.last_limit_count += lc_diff
|
||||
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 = (self.last_sequence * self.samples_per_block
|
||||
+ buffered // self.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 hardware processing time.
|
||||
chip_clock = msg_count + 1
|
||||
self.clock_sync.update(mcu_clock + duration // 2, chip_clock)
|
||||
@@ -142,13 +142,22 @@ def MCU_SPI_from_config(config, mode, pin_option="cs_pin",
|
||||
|
||||
# Helper code for working with devices connected to an MCU via an I2C bus
|
||||
class MCU_I2C:
|
||||
def __init__(self, mcu, bus, addr, speed):
|
||||
def __init__(self, mcu, bus, addr, speed, sw_pins=None):
|
||||
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)
|
||||
mcu.add_config_cmd("config_i2c oid=%d" % (self.oid,))
|
||||
# Generate I2C bus config message
|
||||
if sw_pins is not None:
|
||||
self.config_fmt = (
|
||||
"i2c_set_software_bus oid=%d"
|
||||
" scl_pin=%s sda_pin=%s rate=%d address=%d"
|
||||
% (self.oid, sw_pins[0], sw_pins[1], speed, addr))
|
||||
else:
|
||||
self.config_fmt = (
|
||||
"i2c_set_bus 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
|
||||
@@ -161,8 +170,10 @@ class MCU_I2C:
|
||||
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,))
|
||||
if '%' in self.config_fmt:
|
||||
bus = resolve_bus_name(self.mcu, "i2c_bus", self.bus)
|
||||
self.config_fmt = self.config_fmt % (bus,)
|
||||
self.mcu.add_config_cmd(self.config_fmt)
|
||||
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(
|
||||
@@ -202,13 +213,24 @@ def MCU_I2C_from_config(config, default_addr=None, default_speed=100000):
|
||||
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)
|
||||
# Determine pin from config
|
||||
ppins = config.get_printer().lookup_object("pins")
|
||||
if config.get('i2c_software_scl_pin', None) is not None:
|
||||
sw_pin_names = ['i2c_software_%s_pin' % (name,)
|
||||
for name in ['scl', 'sda']]
|
||||
sw_pin_params = [ppins.lookup_pin(config.get(name), share_type=name)
|
||||
for name in sw_pin_names]
|
||||
sw_pins = tuple([pin_params['pin'] for pin_params in sw_pin_params])
|
||||
bus = None
|
||||
else:
|
||||
bus = config.get('i2c_bus', None)
|
||||
sw_pins = None
|
||||
# Create MCU_I2C object
|
||||
return MCU_I2C(i2c_mcu, bus, addr, speed)
|
||||
return MCU_I2C(i2c_mcu, bus, addr, speed, sw_pins)
|
||||
|
||||
|
||||
######################################################################
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
# Support for button detection and callbacks
|
||||
#
|
||||
# Copyright (C) 2018 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2018-2023 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging
|
||||
@@ -57,10 +57,9 @@ class MCU_buttons:
|
||||
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
|
||||
ack_diff = (params['ack_count'] - ack_count) & 0xff
|
||||
ack_diff -= (ack_diff & 0x80) << 1
|
||||
msg_ack_count = ack_count + ack_diff
|
||||
# Determine new buttons
|
||||
buttons = bytearray(params['state'])
|
||||
new_count = msg_ack_count + len(buttons) - self.ack_count
|
||||
@@ -70,17 +69,17 @@ class MCU_buttons:
|
||||
# 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
|
||||
# Invoke callbacks with this event in main thread
|
||||
btime = params['#receive_time']
|
||||
for button in new_buttons:
|
||||
button ^= self.invert
|
||||
changed = button ^ self.last_button
|
||||
self.last_button = button
|
||||
for mask, shift, callback in self.callbacks:
|
||||
if changed & mask:
|
||||
state = (button & mask) >> shift
|
||||
self.reactor.register_async_callback(
|
||||
(lambda et, c=callback, bt=btime, s=state: c(bt, s)))
|
||||
|
||||
|
||||
######################################################################
|
||||
|
||||
Binary file not shown.
@@ -4,6 +4,8 @@
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
|
||||
NODEID_FIRST = 4
|
||||
|
||||
class PrinterCANBus:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
@@ -11,7 +13,7 @@ class PrinterCANBus:
|
||||
def add_uuid(self, config, canbus_uuid, canbus_iface):
|
||||
if canbus_uuid in self.ids:
|
||||
raise config.error("Duplicate canbus_uuid")
|
||||
new_id = len(self.ids)
|
||||
new_id = len(self.ids) + NODEID_FIRST
|
||||
self.ids[canbus_uuid] = new_id
|
||||
return new_id
|
||||
def get_nodeid(self, canbus_uuid):
|
||||
|
||||
59
klippy/extras/chamber_fan.py
Normal file
59
klippy/extras/chamber_fan.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from . import fan
|
||||
|
||||
PIN_MIN_TIME = 0.100
|
||||
|
||||
class ChamberFan:
|
||||
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.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", ())
|
||||
self.fan_on = True
|
||||
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]
|
||||
def handle_ready(self):
|
||||
reactor = self.printer.get_reactor()
|
||||
reactor.register_timer(self.callback, reactor.monotonic()+PIN_MIN_TIME)
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
gcode.register_command("TOGGLE_CHAMBER_FAN", self.cmd_toggle_chamber_fan)
|
||||
def cmd_toggle_chamber_fan(self, gcmd):
|
||||
self.fan_on = not self.fan_on
|
||||
def get_status(self, eventtime):
|
||||
return self.fan.get_status(eventtime)
|
||||
def callback(self, eventtime):
|
||||
speed = 0.
|
||||
active = False
|
||||
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 not self.fan_on:
|
||||
speed = 0.
|
||||
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 ChamberFan(config)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -24,6 +24,9 @@
|
||||
# + Home All
|
||||
# + Home Z
|
||||
# + Home X/Y
|
||||
# + Z Tilt
|
||||
# + Quad Gantry Lvl
|
||||
# + Bed Mesh
|
||||
# + Steppers off
|
||||
# + Fan: OFF
|
||||
# + Fan speed: 000%
|
||||
@@ -233,6 +236,24 @@ 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
|
||||
@@ -683,7 +704,7 @@ name: Calibration
|
||||
|
||||
[menu __main __setup __calib __delta_calib_auto]
|
||||
type: command
|
||||
enable: {not printer.idle_timeout.state == "Printing"}
|
||||
enable: {(not printer.idle_timeout.state == "Printing") and ('delta_calibrate' in printer)}
|
||||
name: Delta cal. auto
|
||||
gcode:
|
||||
G28
|
||||
@@ -691,12 +712,12 @@ gcode:
|
||||
|
||||
[menu __main __setup __calib __delta_calib_man]
|
||||
type: list
|
||||
enable: {not printer.idle_timeout.state == "Printing"}
|
||||
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"}
|
||||
enable: {(not printer.idle_timeout.state == "Printing") and ('probe' in printer)}
|
||||
name: Bed probe
|
||||
gcode: PROBE
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,8 @@
|
||||
import math, logging
|
||||
import stepper
|
||||
|
||||
TRINAMIC_DRIVERS = ["tmc2130", "tmc2208", "tmc2209", "tmc2240", "tmc2660", "tmc5160"]
|
||||
TRINAMIC_DRIVERS = ["tmc2130", "tmc2208", "tmc2209", "tmc2240", "tmc2660",
|
||||
"tmc5160"]
|
||||
|
||||
# Calculate the trigger phase of a stepper motor
|
||||
class PhaseCalc:
|
||||
|
||||
@@ -234,7 +234,7 @@ class ExcludeObject:
|
||||
|
||||
elif current:
|
||||
if not self.current_object:
|
||||
gcmd.respond_error('There is no current object to cancel')
|
||||
raise self.gcode.error('There is no current object to cancel')
|
||||
|
||||
else:
|
||||
self._exclude_object(self.current_object)
|
||||
|
||||
Binary file not shown.
@@ -15,6 +15,8 @@ class PrinterExtruderStepper:
|
||||
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)
|
||||
|
||||
@@ -30,6 +30,12 @@ class Fan:
|
||||
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)
|
||||
|
||||
@@ -46,6 +52,11 @@ class Fan:
|
||||
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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -43,12 +43,12 @@ class ForceMove:
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_command('STEPPER_BUZZ', self.cmd_STEPPER_BUZZ,
|
||||
desc=self.cmd_STEPPER_BUZZ_help)
|
||||
gcode.register_command('SET_KINEMATIC_POSITION',
|
||||
self.cmd_SET_KINEMATIC_POSITION,
|
||||
desc=self.cmd_SET_KINEMATIC_POSITION_help)
|
||||
if config.getboolean("enable_force_move", False):
|
||||
gcode.register_command('FORCE_MOVE', self.cmd_FORCE_MOVE,
|
||||
desc=self.cmd_FORCE_MOVE_help)
|
||||
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):
|
||||
@@ -86,7 +86,8 @@ class ForceMove:
|
||||
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)
|
||||
self.trapq_finalize_moves(self.trapq, print_time + 99999.9,
|
||||
print_time + 99999.9)
|
||||
stepper.set_trapq(prev_trapq)
|
||||
stepper.set_stepper_kinematics(prev_sk)
|
||||
toolhead.note_kinematic_activity(print_time)
|
||||
@@ -131,7 +132,6 @@ class ForceMove:
|
||||
z = gcmd.get_float('Z', curpos[2])
|
||||
logging.info("SET_KINEMATIC_POSITION pos=%.3f,%.3f,%.3f", x, y, z)
|
||||
toolhead.set_position([x, y, z, curpos[3]], homing_axes=(0, 1, 2))
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return ForceMove(config)
|
||||
|
||||
Binary file not shown.
@@ -10,9 +10,22 @@ import math
|
||||
|
||||
# Coordinates created by this are converted into G1 commands.
|
||||
#
|
||||
# note: only IJ version available
|
||||
# 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)
|
||||
@@ -22,12 +35,30 @@ class ArcSupport:
|
||||
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']:
|
||||
@@ -35,21 +66,33 @@ class ArcSupport:
|
||||
currentPos = gcodestatus['gcode_position']
|
||||
|
||||
# Parse parameters
|
||||
asX = gcmd.get_float("X", currentPos[0])
|
||||
asY = gcmd.get_float("Y", currentPos[1])
|
||||
asZ = gcmd.get_float("Z", currentPos[2])
|
||||
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")
|
||||
asI = gcmd.get_float("I", 0.)
|
||||
asJ = gcmd.get_float("J", 0.)
|
||||
if not asI and not asJ:
|
||||
raise gcmd.error("G2/G3 neither I nor J given")
|
||||
|
||||
# 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 to
|
||||
coords = self.planArc(currentPos, [asX, asY, asZ], [asI, asJ],
|
||||
clockwise)
|
||||
# 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']:
|
||||
@@ -74,37 +117,37 @@ class ArcSupport:
|
||||
# The arc is approximated by generating many small linear segments.
|
||||
# The length of each segment is configured in MM_PER_ARC_SEGMENT
|
||||
# Arcs smaller then this value, will be a Line only
|
||||
def planArc(self, currentPos, targetPos, offset, clockwise):
|
||||
#
|
||||
# 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
|
||||
X_AXIS = 0
|
||||
Y_AXIS = 1
|
||||
Z_AXIS = 2
|
||||
|
||||
# Radius vector from center to current location
|
||||
r_P = -offset[0]
|
||||
r_Q = -offset[1]
|
||||
|
||||
# Determine angular travel
|
||||
center_P = currentPos[X_AXIS] - r_P
|
||||
center_Q = currentPos[Y_AXIS] - r_Q
|
||||
rt_X = targetPos[X_AXIS] - center_P
|
||||
rt_Y = targetPos[Y_AXIS] - center_Q
|
||||
angular_travel = math.atan2(r_P * rt_Y - r_Q * rt_X,
|
||||
r_P * rt_X + r_Q * rt_Y)
|
||||
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[X_AXIS] == targetPos[X_AXIS]
|
||||
and currentPos[Y_AXIS] == targetPos[Y_AXIS]):
|
||||
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[Z_AXIS] - currentPos[Z_AXIS]
|
||||
linear_travel = targetPos[helical_axis] - currentPos[helical_axis]
|
||||
radius = math.hypot(r_P, r_Q)
|
||||
flat_mm = radius * angular_travel
|
||||
if linear_travel:
|
||||
@@ -118,14 +161,18 @@ class ArcSupport:
|
||||
linear_per_segment = linear_travel / segments
|
||||
coords = []
|
||||
for i in range(1, int(segments)):
|
||||
dist_Z = i * linear_per_segment
|
||||
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
|
||||
|
||||
c = [center_P + r_P, center_Q + r_Q, currentPos[Z_AXIS] + dist_Z]
|
||||
coords.append(c)
|
||||
# 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
|
||||
|
||||
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
# 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 traceback, logging, ast, copy, json
|
||||
import jinja2
|
||||
|
||||
|
||||
@@ -144,12 +144,13 @@ class GCodeMacro:
|
||||
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:
|
||||
literal = ast.literal_eval(config.get(option))
|
||||
json.dumps(literal, separators=(',', ':'))
|
||||
self.variables[option[len(prefix):]] = literal
|
||||
except (SyntaxError, TypeError, ValueError) as e:
|
||||
raise config.error(
|
||||
"Option '%s' in section '%s' is not a valid literal" % (
|
||||
option, config.get_name()))
|
||||
"Option '%s' in section '%s' is not a valid literal: %s" % (
|
||||
option, config.get_name(), e))
|
||||
def handle_connect(self):
|
||||
prev_cmd = self.gcode.register_command(self.alias, None)
|
||||
if prev_cmd is None:
|
||||
@@ -169,8 +170,10 @@ class GCodeMacro:
|
||||
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,))
|
||||
json.dumps(literal, separators=(',', ':'))
|
||||
except (SyntaxError, TypeError, ValueError) as e:
|
||||
raise gcmd.error("Unable to parse '%s' as a literal: %s" %
|
||||
(value, e))
|
||||
v = dict(self.variables)
|
||||
v[variable] = literal
|
||||
self.variables = v
|
||||
|
||||
Binary file not shown.
15
klippy/extras/gcode_macro_break.py
Normal file
15
klippy/extras/gcode_macro_break.py
Normal file
@@ -0,0 +1,15 @@
|
||||
class GCodeMacroBreaker:
|
||||
def __init__(self, config):
|
||||
# Gcode macro interupt
|
||||
self.printer = config.get_printer()
|
||||
webhooks = self.printer.lookup_object('webhooks')
|
||||
webhooks.register_endpoint("breakmacro", self._handle_breakmacro)
|
||||
webhooks.register_endpoint("resumemacro", self._handle_resumemacro)
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
def _handle_breakmacro(self, web_request):
|
||||
self.gcode.break_flag = True
|
||||
def _handle_resumemacro(self, web_request):
|
||||
self.gcode.break_flag = False
|
||||
|
||||
def load_config(config):
|
||||
return GCodeMacroBreaker(config)
|
||||
@@ -49,6 +49,9 @@ class GCodeMove:
|
||||
self.saved_states = {}
|
||||
self.move_transform = self.move_with_transform = None
|
||||
self.position_with_transform = (lambda: [0., 0., 0., 0.])
|
||||
# Save and load z offset
|
||||
gcode.register_command('SAVE_ZOFFSET_TO_VARIABLE', self.cmd_SAVE_ZOFFSET_TO_VARIABLE)
|
||||
gcode.register_command('LOAD_ZOFFSET_FROM_VARIABLE', self.cmd_LOAD_ZOFFSET_FROM_VARIABLE)
|
||||
def _handle_ready(self):
|
||||
self.is_printer_ready = True
|
||||
if self.move_transform is None:
|
||||
@@ -271,6 +274,16 @@ class GCodeMove:
|
||||
"gcode homing: %s"
|
||||
% (mcu_pos, stepper_pos, kin_pos, toolhead_pos,
|
||||
gcode_pos, base_pos, homing_pos))
|
||||
|
||||
def cmd_SAVE_ZOFFSET_TO_VARIABLE(self, gcmd):
|
||||
variables = self.printer.lookup_object("save_variables")
|
||||
gcode_move = self.printer.lookup_object("gcode_move")
|
||||
variables.save_variable('Variables', 'z_offset', gcode_move.homing_position[2])
|
||||
|
||||
def cmd_LOAD_ZOFFSET_FROM_VARIABLE(self, gcmd):
|
||||
variables = self.printer.lookup_object("save_variables")
|
||||
gcode_move = self.printer.lookup_object("gcode_move")
|
||||
gcode_move.homing_position[2] = float(variables.load_variable('Variables', 'z_offset'))
|
||||
|
||||
def load_config(config):
|
||||
return GCodeMove(config)
|
||||
|
||||
Binary file not shown.
87
klippy/extras/gcode_shell_command.py
Normal file
87
klippy/extras/gcode_shell_command.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# Run a shell command via gcode
|
||||
#
|
||||
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import logging
|
||||
|
||||
class ShellCommand:
|
||||
def __init__(self, config):
|
||||
self.name = config.get_name().split()[-1]
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
cmd = config.get('command')
|
||||
cmd = os.path.expanduser(cmd)
|
||||
self.command = shlex.split(cmd)
|
||||
self.timeout = config.getfloat('timeout', 2., above=0.)
|
||||
self.verbose = config.getboolean('verbose', True)
|
||||
self.proc_fd = None
|
||||
self.partial_output = ""
|
||||
self.gcode.register_mux_command(
|
||||
"RUN_SHELL_COMMAND", "CMD", self.name,
|
||||
self.cmd_RUN_SHELL_COMMAND,
|
||||
desc=self.cmd_RUN_SHELL_COMMAND_help)
|
||||
|
||||
def _process_output(self, eventime):
|
||||
if self.proc_fd is None:
|
||||
return
|
||||
try:
|
||||
data = os.read(self.proc_fd, 4096)
|
||||
except Exception:
|
||||
pass
|
||||
data = self.partial_output + data.decode()
|
||||
if '\n' not in data:
|
||||
self.partial_output = data
|
||||
return
|
||||
elif data[-1] != '\n':
|
||||
split = data.rfind('\n') + 1
|
||||
self.partial_output = data[split:]
|
||||
data = data[:split]
|
||||
else:
|
||||
self.partial_output = ""
|
||||
self.gcode.respond_info(data)
|
||||
|
||||
cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command"
|
||||
def cmd_RUN_SHELL_COMMAND(self, params):
|
||||
gcode_params = params.get('PARAMS','')
|
||||
gcode_params = shlex.split(gcode_params)
|
||||
reactor = self.printer.get_reactor()
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
self.command + gcode_params, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"shell_command: Command {%s} failed" % (self.name))
|
||||
raise self.gcode.error("Error running command {%s}" % (self.name))
|
||||
if self.verbose:
|
||||
self.proc_fd = proc.stdout.fileno()
|
||||
self.gcode.respond_info("Running Command {%s}...:" % (self.name))
|
||||
hdl = reactor.register_fd(self.proc_fd, self._process_output)
|
||||
eventtime = reactor.monotonic()
|
||||
endtime = eventtime + self.timeout
|
||||
complete = False
|
||||
while eventtime < endtime:
|
||||
eventtime = reactor.pause(eventtime + .05)
|
||||
if proc.poll() is not None:
|
||||
complete = True
|
||||
break
|
||||
if not complete:
|
||||
proc.terminate()
|
||||
if self.verbose:
|
||||
if self.partial_output:
|
||||
self.gcode.respond_info(self.partial_output)
|
||||
self.partial_output = ""
|
||||
if complete:
|
||||
msg = "Command {%s} finished\n" % (self.name)
|
||||
else:
|
||||
msg = "Command {%s} timed out" % (self.name)
|
||||
self.gcode.respond_info(msg)
|
||||
reactor.unregister_fd(hdl)
|
||||
self.proc_fd = None
|
||||
|
||||
|
||||
def load_config_prefix(config):
|
||||
return ShellCommand(config)
|
||||
@@ -124,6 +124,7 @@ class HallFilamentWidthSensor:
|
||||
# Update filament array for lastFilamentWidthReading
|
||||
self.update_filament_array(last_epos)
|
||||
# Check runout
|
||||
# self.gcode.respond_info("Check diameter: {}".format(self.diameter))
|
||||
self.runout_helper.note_filament_present(
|
||||
self.diameter > self.runout_dia)
|
||||
# Does filament exists
|
||||
@@ -134,23 +135,23 @@ class HallFilamentWidthSensor:
|
||||
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")
|
||||
# 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.gcode.run_script("M221 S100")
|
||||
self.filament_array = []
|
||||
|
||||
if self.is_active:
|
||||
@@ -171,7 +172,7 @@ class HallFilamentWidthSensor:
|
||||
self.filament_array = []
|
||||
gcmd.respond_info("Filament width measurements cleared!")
|
||||
# Set extrude multiplier to 100%
|
||||
self.gcode.run_script_from_command("M221 S100")
|
||||
# self.gcode.run_script_from_command("M221 S100")
|
||||
|
||||
def cmd_M405(self, gcmd):
|
||||
response = "Filament width sensor Turned On"
|
||||
@@ -196,7 +197,7 @@ class HallFilamentWidthSensor:
|
||||
# Clear filament array
|
||||
self.filament_array = []
|
||||
# Set extrude multiplier to 100%
|
||||
self.gcode.run_script_from_command("M221 S100")
|
||||
# self.gcode.run_script_from_command("M221 S100")
|
||||
gcmd.respond_info(response)
|
||||
|
||||
def cmd_Get_Raw_Values(self, gcmd):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -230,6 +230,7 @@ class PrinterHeaters:
|
||||
self.gcode_id_to_sensor = {}
|
||||
self.available_heaters = []
|
||||
self.available_sensors = []
|
||||
self.available_monitors = []
|
||||
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",
|
||||
@@ -241,6 +242,16 @@ class PrinterHeaters:
|
||||
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)
|
||||
# Wait heater interupt
|
||||
webhooks = self.printer.lookup_object('webhooks')
|
||||
webhooks.register_endpoint("breakheater", self._handle_breakheater)
|
||||
self.break_flag=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.break_flag = True
|
||||
def load_config(self, config):
|
||||
self.have_load_sensors = True
|
||||
# Load default temperature sensors
|
||||
@@ -293,9 +304,12 @@ class PrinterHeaters:
|
||||
raise self.printer.config_error(
|
||||
"G-Code sensor id %s already registered" % (gcode_id,))
|
||||
self.gcode_id_to_sensor[gcode_id] = psensor
|
||||
def register_monitor(self, config):
|
||||
self.available_monitors.append(config.get_name())
|
||||
def get_status(self, eventtime):
|
||||
return {'available_heaters': self.available_heaters,
|
||||
'available_sensors': self.available_sensors}
|
||||
'available_sensors': self.available_sensors,
|
||||
'available_monitors': self.available_monitors}
|
||||
def turn_off_all_heaters(self, print_time=0.):
|
||||
for heater in self.heaters.values():
|
||||
heater.set_temp(0.)
|
||||
@@ -330,7 +344,11 @@ class PrinterHeaters:
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
reactor = self.printer.get_reactor()
|
||||
eventtime = reactor.monotonic()
|
||||
self.break_flag = False
|
||||
while not self.printer.is_shutdown() and heater.check_busy(eventtime):
|
||||
if self.break_flag:
|
||||
self.break_flag = False
|
||||
break
|
||||
print_time = toolhead.get_last_move_time()
|
||||
gcode.respond_raw(self._get_temp(eventtime))
|
||||
eventtime = reactor.pause(eventtime + 1.)
|
||||
@@ -359,7 +377,7 @@ class PrinterHeaters:
|
||||
toolhead = self.printer.lookup_object("toolhead")
|
||||
reactor = self.printer.get_reactor()
|
||||
eventtime = reactor.monotonic()
|
||||
while not self.printer.is_shutdown():
|
||||
while not self.printer.is_shutdown() and not self.break_flag:
|
||||
temp, target = sensor.get_temp(eventtime)
|
||||
if temp >= min_temp and temp <= max_temp:
|
||||
return
|
||||
|
||||
Binary file not shown.
@@ -250,8 +250,8 @@ class PrinterHoming:
|
||||
"Probing failed due to printer shutdown")
|
||||
raise
|
||||
if hmove.check_no_movement() is not None:
|
||||
raise self.printer.command_error(
|
||||
"Probe triggered prior to movement")
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.respond_info('Probe triggered prior to movement')
|
||||
return epos
|
||||
def cmd_G28(self, gcmd):
|
||||
# Move to origin
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -126,7 +126,7 @@ class HTU21D:
|
||||
rdevId |= response[1]
|
||||
checksum = response[2]
|
||||
if self._chekCRC8(rdevId) != checksum:
|
||||
logging.warn("htu21d: Reading deviceId !Checksum error!")
|
||||
logging.warning("htu21d: Reading deviceId !Checksum error!")
|
||||
rdevId = rdevId >> 8
|
||||
deviceId_list = list(
|
||||
filter(
|
||||
@@ -135,10 +135,10 @@ class HTU21D:
|
||||
if len(deviceId_list) != 0:
|
||||
logging.info("htu21d: Found Device Type %s" % deviceId_list[0])
|
||||
else:
|
||||
logging.warn("htu21d: Unknown Device ID %#x " % rdevId)
|
||||
logging.warning("htu21d: Unknown Device ID %#x " % rdevId)
|
||||
|
||||
if(self.deviceId != deviceId_list[0]):
|
||||
logging.warn(
|
||||
if self.deviceId != deviceId_list[0]:
|
||||
logging.warning(
|
||||
"htu21d: Found device %s. Forcing to type %s as config.",
|
||||
deviceId_list[0],self.deviceId)
|
||||
|
||||
@@ -169,7 +169,9 @@ class HTU21D:
|
||||
rtemp = response[0] << 8
|
||||
rtemp |= response[1]
|
||||
if self._chekCRC8(rtemp) != response[2]:
|
||||
logging.warn("htu21d: Checksum error on Temperature reading!")
|
||||
logging.warning(
|
||||
"htu21d: Checksum error on Temperature reading!"
|
||||
)
|
||||
else:
|
||||
self.temp = (0.002681 * float(rtemp) - 46.85)
|
||||
logging.debug("htu21d: Temperature %.2f " % self.temp)
|
||||
@@ -190,7 +192,7 @@ class HTU21D:
|
||||
rhumid = response[0] << 8
|
||||
rhumid|= response[1]
|
||||
if self._chekCRC8(rhumid) != response[2]:
|
||||
logging.warn("htu21d: Checksum error on Humidity reading!")
|
||||
logging.warning("htu21d: Checksum error on Humidity reading!")
|
||||
else:
|
||||
#clear status bits,
|
||||
# humidity always returns xxxxxx10 in the LSB field
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -61,7 +61,6 @@ class AxisInputShaper:
|
||||
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(
|
||||
@@ -71,10 +70,6 @@ class AxisInputShaper:
|
||||
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)
|
||||
@@ -98,7 +93,7 @@ class InputShaper:
|
||||
self.toolhead = None
|
||||
self.shapers = [AxisInputShaper('x', config),
|
||||
AxisInputShaper('y', config)]
|
||||
self.stepper_kinematics = []
|
||||
self.input_shaper_stepper_kinematics = []
|
||||
self.orig_stepper_kinematics = []
|
||||
# Register gcode commands
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
@@ -109,38 +104,51 @@ class InputShaper:
|
||||
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 _get_input_shaper_stepper_kinematics(self, stepper):
|
||||
# Lookup stepper kinematics
|
||||
sk = stepper.get_stepper_kinematics()
|
||||
if sk in self.orig_stepper_kinematics:
|
||||
# Already processed this stepper kinematics unsuccessfully
|
||||
return None
|
||||
if sk in self.input_shaper_stepper_kinematics:
|
||||
return sk
|
||||
self.orig_stepper_kinematics.append(sk)
|
||||
ffi_main, ffi_lib = chelper.get_ffi()
|
||||
is_sk = ffi_main.gc(ffi_lib.input_shaper_alloc(), ffi_lib.free)
|
||||
stepper.set_stepper_kinematics(is_sk)
|
||||
res = ffi_lib.input_shaper_set_sk(is_sk, sk)
|
||||
if res < 0:
|
||||
stepper.set_stepper_kinematics(sk)
|
||||
return None
|
||||
self.input_shaper_stepper_kinematics.append(is_sk)
|
||||
return is_sk
|
||||
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:
|
||||
ffi_main, ffi_lib = chelper.get_ffi()
|
||||
kin = self.toolhead.get_kinematics()
|
||||
failed_shapers = []
|
||||
for s in kin.get_steppers():
|
||||
if s.get_trapq() is None:
|
||||
continue
|
||||
is_sk = self._get_input_shaper_stepper_kinematics(s)
|
||||
if is_sk is None:
|
||||
continue
|
||||
old_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
|
||||
for shaper in self.shapers:
|
||||
if shaper in failed:
|
||||
if shaper in failed_shapers:
|
||||
continue
|
||||
if not shaper.set_shaper_kinematics(sk):
|
||||
failed.append(shaper)
|
||||
if failed:
|
||||
if not shaper.set_shaper_kinematics(is_sk):
|
||||
failed_shapers.append(shaper)
|
||||
new_delay = ffi_lib.input_shaper_get_step_generation_window(is_sk)
|
||||
if old_delay != new_delay:
|
||||
self.toolhead.note_step_generation_scan_time(new_delay,
|
||||
old_delay)
|
||||
if failed_shapers:
|
||||
error = error or self.printer.command_error
|
||||
raise error("Failed to configure shaper(s) %s with given parameters"
|
||||
% (', '.join([s.get_name() for s in failed])))
|
||||
% (', '.join([s.get_name() for s in failed_shapers])))
|
||||
def disable_shaping(self):
|
||||
for shaper in self.shapers:
|
||||
shaper.disable_shaping()
|
||||
@@ -151,10 +159,9 @@ class InputShaper:
|
||||
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:
|
||||
if gcmd.get_command_parameters():
|
||||
for shaper in self.shapers:
|
||||
shaper.update(gcmd)
|
||||
self._update_input_shaping()
|
||||
for shaper in self.shapers:
|
||||
shaper.report(gcmd)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
201
klippy/extras/lis2dw.py
Normal file
201
klippy/extras/lis2dw.py
Normal file
@@ -0,0 +1,201 @@
|
||||
# Support for reading acceleration data from an LIS2DW chip
|
||||
#
|
||||
# Copyright (C) 2023 Zhou.XianMing <zhouxm@biqu3d.com>
|
||||
# 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
|
||||
from . import bus, adxl345, bulk_sensor
|
||||
|
||||
# LIS2DW registers
|
||||
REG_LIS2DW_WHO_AM_I_ADDR = 0x0F
|
||||
REG_LIS2DW_CTRL_REG1_ADDR = 0x20
|
||||
REG_LIS2DW_CTRL_REG2_ADDR = 0x21
|
||||
REG_LIS2DW_CTRL_REG3_ADDR = 0x22
|
||||
REG_LIS2DW_CTRL_REG6_ADDR = 0x25
|
||||
REG_LIS2DW_STATUS_REG_ADDR = 0x27
|
||||
REG_LIS2DW_OUT_XL_ADDR = 0x28
|
||||
REG_LIS2DW_OUT_XH_ADDR = 0x29
|
||||
REG_LIS2DW_OUT_YL_ADDR = 0x2A
|
||||
REG_LIS2DW_OUT_YH_ADDR = 0x2B
|
||||
REG_LIS2DW_OUT_ZL_ADDR = 0x2C
|
||||
REG_LIS2DW_OUT_ZH_ADDR = 0x2D
|
||||
REG_LIS2DW_FIFO_CTRL = 0x2E
|
||||
REG_LIS2DW_FIFO_SAMPLES = 0x2F
|
||||
REG_MOD_READ = 0x80
|
||||
# REG_MOD_MULTI = 0x40
|
||||
|
||||
LIS2DW_DEV_ID = 0x44
|
||||
|
||||
FREEFALL_ACCEL = 9.80665
|
||||
SCALE = FREEFALL_ACCEL * 1.952 / 4
|
||||
|
||||
MIN_MSG_TIME = 0.100
|
||||
|
||||
BYTES_PER_SAMPLE = 6
|
||||
SAMPLES_PER_BLOCK = 8
|
||||
|
||||
BATCH_UPDATES = 0.100
|
||||
|
||||
# Printer class that controls LIS2DW chip
|
||||
class LIS2DW:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
adxl345.AccelCommandHelper(config, self)
|
||||
self.axes_map = adxl345.read_axes_map(config)
|
||||
self.data_rate = 1600
|
||||
# Setup mcu sensor_lis2dw 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_lis2dw_cmd = self.query_lis2dw_end_cmd = None
|
||||
self.query_lis2dw_status_cmd = None
|
||||
mcu.add_config_cmd("config_lis2dw oid=%d spi_oid=%d"
|
||||
% (oid, self.spi.get_oid()))
|
||||
mcu.add_config_cmd("query_lis2dw oid=%d clock=0 rest_ticks=0"
|
||||
% (oid,), on_restart=True)
|
||||
mcu.register_config_callback(self._build_config)
|
||||
self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, "lis2dw_data", oid)
|
||||
# Clock tracking
|
||||
chip_smooth = self.data_rate * BATCH_UPDATES * 2
|
||||
self.clock_sync = bulk_sensor.ClockSyncRegression(mcu, chip_smooth)
|
||||
self.clock_updater = bulk_sensor.ChipClockUpdater(self.clock_sync,
|
||||
BYTES_PER_SAMPLE)
|
||||
self.last_error_count = 0
|
||||
# Process messages in batches
|
||||
self.batch_bulk = bulk_sensor.BatchBulkHelper(
|
||||
self.printer, self._process_batch,
|
||||
self._start_measurements, self._finish_measurements, BATCH_UPDATES)
|
||||
self.name = config.get_name().split()[-1]
|
||||
hdr = ('time', 'x_acceleration', 'y_acceleration', 'z_acceleration')
|
||||
self.batch_bulk.add_mux_endpoint("lis2dw/dump_lis2dw", "sensor",
|
||||
self.name, {'header': hdr})
|
||||
|
||||
def _build_config(self):
|
||||
cmdqueue = self.spi.get_command_queue()
|
||||
self.query_lis2dw_cmd = self.mcu.lookup_command(
|
||||
"query_lis2dw oid=%c clock=%u rest_ticks=%u", cq=cmdqueue)
|
||||
self.query_lis2dw_end_cmd = self.mcu.lookup_query_command(
|
||||
"query_lis2dw oid=%c clock=%u rest_ticks=%u",
|
||||
"lis2dw_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_lis2dw_status_cmd = self.mcu.lookup_query_command(
|
||||
"query_lis2dw_status oid=%c",
|
||||
"lis2dw_status oid=%c clock=%u query_ticks=%u next_sequence=%hu"
|
||||
" buffered=%c fifo=%c limit_count=%hu", oid=self.oid, cq=cmdqueue)
|
||||
def read_reg(self, reg):
|
||||
params = self.spi.spi_transfer([reg | REG_MOD_READ, 0x00])
|
||||
response = bytearray(params['response'])
|
||||
return response[1]
|
||||
def set_reg(self, reg, val, minclock=0):
|
||||
self.spi.spi_send([reg, val & 0xFF], minclock=minclock)
|
||||
stored_val = self.read_reg(reg)
|
||||
if stored_val != val:
|
||||
raise self.printer.command_error(
|
||||
"Failed to set LIS2DW register [0x%x] to 0x%x: got 0x%x. "
|
||||
"This is generally indicative of connection problems "
|
||||
"(e.g. faulty wiring) or a faulty lis2dw chip." % (
|
||||
reg, val, stored_val))
|
||||
def start_internal_client(self):
|
||||
aqh = adxl345.AccelQueryHelper(self.printer)
|
||||
self.batch_bulk.add_client(aqh.handle_batch)
|
||||
return aqh
|
||||
# Measurement decoding
|
||||
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.clock_updater.get_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 = (params['sequence'] - last_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, xhigh, ylow, yhigh, zlow, zhigh = d_xyz
|
||||
# Merge and perform twos-complement
|
||||
|
||||
rx = (((xhigh << 8) | xlow)) - ((xhigh & 0x80) << 9)
|
||||
ry = (((yhigh << 8) | ylow)) - ((yhigh & 0x80) << 9)
|
||||
rz = (((zhigh << 8) | zlow)) - ((zhigh & 0x80) << 9)
|
||||
|
||||
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):
|
||||
params = self.query_lis2dw_status_cmd.send([self.oid],
|
||||
minclock=minclock)
|
||||
self.clock_updater.update_clock(params)
|
||||
# Start, stop, and process message batches
|
||||
def _start_measurements(self):
|
||||
# In case of miswiring, testing LIS2DW device ID prevents treating
|
||||
# noise or wrong signal as a correctly initialized device
|
||||
dev_id = self.read_reg(REG_LIS2DW_WHO_AM_I_ADDR)
|
||||
logging.info("lis2dw_dev_id: %x", dev_id)
|
||||
if dev_id != LIS2DW_DEV_ID:
|
||||
raise self.printer.command_error(
|
||||
"Invalid lis2dw id (got %x vs %x).\n"
|
||||
"This is generally indicative of connection problems\n"
|
||||
"(e.g. faulty wiring) or a faulty lis2dw chip."
|
||||
% (dev_id, LIS2DW_DEV_ID))
|
||||
# Setup chip in requested query rate
|
||||
# ODR/2, +-16g, low-pass filter, Low-noise abled
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG6_ADDR, 0x34)
|
||||
# Continuous mode: If the FIFO is full
|
||||
# the new sample overwrites the older sample.
|
||||
self.set_reg(REG_LIS2DW_FIFO_CTRL, 0xC0)
|
||||
# High-Performance / Low-Power mode 1600/200 Hz
|
||||
# High-Performance Mode (14-bit resolution)
|
||||
self.set_reg(REG_LIS2DW_CTRL_REG1_ADDR, 0x94)
|
||||
|
||||
# Start bulk reading
|
||||
self.bulk_queue.clear_samples()
|
||||
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_lis2dw_cmd.send([self.oid, reqclock, rest_ticks],
|
||||
reqclock=reqclock)
|
||||
logging.info("LIS2DW starting '%s' measurements", self.name)
|
||||
# Initialize clock tracking
|
||||
self.clock_updater.note_start(reqclock)
|
||||
self._update_clock(minclock=reqclock)
|
||||
self.clock_updater.clear_duration_filter()
|
||||
self.last_error_count = 0
|
||||
def _finish_measurements(self):
|
||||
# Halt bulk reading
|
||||
params = self.query_lis2dw_end_cmd.send([self.oid, 0, 0])
|
||||
self.bulk_queue.clear_samples()
|
||||
logging.info("LIS2DW finished '%s' measurements", self.name)
|
||||
self.set_reg(REG_LIS2DW_FIFO_CTRL, 0x00)
|
||||
def _process_batch(self, eventtime):
|
||||
self._update_clock()
|
||||
raw_samples = self.bulk_queue.pull_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.clock_updater.get_last_limit_count()}
|
||||
|
||||
def load_config(config):
|
||||
return LIS2DW(config)
|
||||
|
||||
def load_config_prefix(config):
|
||||
return LIS2DW(config)
|
||||
Binary file not shown.
@@ -13,9 +13,25 @@ class ManualProbe:
|
||||
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,
|
||||
@@ -24,6 +40,12 @@ class ManualProbe:
|
||||
'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:
|
||||
@@ -66,6 +88,29 @@ class ManualProbe:
|
||||
"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
|
||||
|
||||
Binary file not shown.
@@ -67,7 +67,8 @@ class ManualStepper:
|
||||
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)
|
||||
self.trapq_finalize_moves(self.trapq, self.next_cmd_time + 99999.9,
|
||||
self.next_cmd_time + 99999.9)
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.note_kinematic_activity(self.next_cmd_time)
|
||||
if sync:
|
||||
|
||||
@@ -68,17 +68,30 @@ class SoftwareI2C:
|
||||
|
||||
class mcp4018:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.i2c = SoftwareI2C(config, 0x2f)
|
||||
self.scale = config.getfloat('scale', 1., above=0.)
|
||||
self.start_value = config.getfloat('wiper',
|
||||
minval=0., maxval=self.scale)
|
||||
config.get_printer().register_event_handler("klippy:connect",
|
||||
self.handle_connect)
|
||||
# Register commands
|
||||
self.name = config.get_name().split()[1]
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
gcode.register_mux_command("SET_DIGIPOT", "DIGIPOT", self.name,
|
||||
self.cmd_SET_DIGIPOT,
|
||||
desc=self.cmd_SET_DIGIPOT_help)
|
||||
def handle_connect(self):
|
||||
self.set_dac(self.start_value)
|
||||
def set_dac(self, value):
|
||||
val = int(value * 127. / self.scale + .5)
|
||||
self.i2c.i2c_write([val])
|
||||
|
||||
cmd_SET_DIGIPOT_help = "Set digipot value"
|
||||
def cmd_SET_DIGIPOT(self, gcmd):
|
||||
wiper = gcmd.get_float('WIPER', minval=0., maxval=self.scale)
|
||||
if wiper is not None:
|
||||
self.set_dac(wiper)
|
||||
gcmd.respond_info("New value for DIGIPOT = %s, wiper = %.2f"
|
||||
% (self.name, wiper))
|
||||
def load_config_prefix(config):
|
||||
return mcp4018(config)
|
||||
|
||||
@@ -5,110 +5,19 @@
|
||||
# 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()
|
||||
from . import bulk_sensor
|
||||
|
||||
# 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)
|
||||
self.last_batch_clock = 0
|
||||
self.batch_bulk = bulk_sensor.BatchBulkHelper(printer,
|
||||
self._process_batch)
|
||||
api_resp = {'header': ('interval', 'count', 'add')}
|
||||
self.batch_bulk.add_mux_endpoint("motion_report/dump_stepper", "name",
|
||||
mcu_stepper.get_name(), api_resp)
|
||||
def get_step_queue(self, start_clock, end_clock):
|
||||
mcu_stepper = self.mcu_stepper
|
||||
res = []
|
||||
@@ -134,15 +43,15 @@ class DumpStepper:
|
||||
% (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)
|
||||
def _process_batch(self, eventtime):
|
||||
data, cdata = self.get_step_queue(self.last_batch_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
|
||||
self.last_batch_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)
|
||||
@@ -154,10 +63,6 @@ class DumpStepper:
|
||||
"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.
|
||||
|
||||
@@ -167,11 +72,13 @@ class DumpTrapQ:
|
||||
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)
|
||||
self.last_batch_msg = (0., 0.)
|
||||
self.batch_bulk = bulk_sensor.BatchBulkHelper(printer,
|
||||
self._process_batch)
|
||||
api_resp = {'header': ('time', 'duration', 'start_velocity',
|
||||
'acceleration', 'start_position', 'direction')}
|
||||
self.batch_bulk.add_mux_endpoint("motion_report/dump_trapq",
|
||||
"name", name, api_resp)
|
||||
def extract_trapq(self, start_time, end_time):
|
||||
ffi_main, ffi_lib = chelper.get_ffi()
|
||||
res = []
|
||||
@@ -210,23 +117,18 @@ class DumpTrapQ:
|
||||
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)
|
||||
def _process_batch(self, eventtime):
|
||||
qtime = self.last_batch_msg[0] + min(self.last_batch_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:
|
||||
if d and d[0] == self.last_batch_msg:
|
||||
d.pop(0)
|
||||
if not d:
|
||||
return {}
|
||||
self.last_api_msg = d[-1]
|
||||
self.last_batch_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
|
||||
|
||||
|
||||
Binary file not shown.
@@ -4,13 +4,21 @@
|
||||
# Copyright (C) 2020-2021 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import logging, time, collections, threading, multiprocessing, os
|
||||
from . import bus, motion_report, adxl345
|
||||
import logging
|
||||
from . import bus, adxl345, bulk_sensor
|
||||
|
||||
MPU9250_ADDR = 0x68
|
||||
|
||||
MPU9250_DEV_ID = 0x73
|
||||
MPU6050_DEV_ID = 0x68
|
||||
MPU_DEV_IDS = {
|
||||
0x74: "mpu-9515",
|
||||
0x73: "mpu-9255",
|
||||
0x71: "mpu-9250",
|
||||
0x70: "mpu-6500",
|
||||
0x68: "mpu-6050",
|
||||
#everything above are normal MPU IDs
|
||||
0x75: "mpu-unknown (DEFECTIVE! USE WITH CAUTION!)",
|
||||
0x69: "mpu-unknown (DEFECTIVE! USE WITH CAUTION!)",
|
||||
}
|
||||
|
||||
# MPU9250 registers
|
||||
REG_DEVID = 0x75
|
||||
@@ -39,32 +47,22 @@ SCALE = 0.000244140625 * FREEFALL_ACCEL
|
||||
|
||||
FIFO_SIZE = 512
|
||||
|
||||
Accel_Measurement = collections.namedtuple(
|
||||
'Accel_Measurement', ('time', 'accel_x', 'accel_y', 'accel_z'))
|
||||
|
||||
MIN_MSG_TIME = 0.100
|
||||
|
||||
BYTES_PER_SAMPLE = 6
|
||||
SAMPLES_PER_BLOCK = 8
|
||||
|
||||
BATCH_UPDATES = 0.100
|
||||
|
||||
# Printer class that controls MPU9250 chip
|
||||
class MPU9250:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
adxl345.AccelCommandHelper(config, self)
|
||||
self.query_rate = 0
|
||||
am = {'x': (0, SCALE), 'y': (1, SCALE), 'z': (2, SCALE),
|
||||
'-x': (0, -SCALE), '-y': (1, -SCALE), '-z': (2, -SCALE)}
|
||||
axes_map = config.getlist('axes_map', ('x','y','z'), count=3)
|
||||
if any([a not in am for a in axes_map]):
|
||||
raise config.error("Invalid mpu9250 axes_map parameter")
|
||||
self.axes_map = [am[a.strip()] for a in axes_map]
|
||||
self.axes_map = adxl345.read_axes_map(config)
|
||||
self.data_rate = config.getint('rate', 4000)
|
||||
if self.data_rate not in SAMPLE_RATE_DIVS:
|
||||
raise config.error("Invalid rate parameter: %d" % (self.data_rate,))
|
||||
# Measurement storage (accessed from background thread)
|
||||
self.lock = threading.Lock()
|
||||
self.raw_samples = []
|
||||
# Setup mcu sensor_mpu9250 bulk query code
|
||||
self.i2c = bus.MCU_I2C_from_config(config,
|
||||
default_addr=MPU9250_ADDR,
|
||||
@@ -74,18 +72,21 @@ class MPU9250:
|
||||
self.query_mpu9250_cmd = self.query_mpu9250_end_cmd = None
|
||||
self.query_mpu9250_status_cmd = None
|
||||
mcu.register_config_callback(self._build_config)
|
||||
mcu.register_response(self._handle_mpu9250_data, "mpu9250_data", oid)
|
||||
self.bulk_queue = bulk_sensor.BulkDataQueue(mcu, "mpu9250_data", oid)
|
||||
# Clock tracking
|
||||
self.last_sequence = self.max_query_duration = 0
|
||||
self.last_limit_count = self.last_error_count = 0
|
||||
self.clock_sync = adxl345.ClockSyncRegression(self.mcu, 640)
|
||||
# API server endpoints
|
||||
self.api_dump = motion_report.APIDumpHelper(
|
||||
self.printer, self._api_update, self._api_startstop, 0.100)
|
||||
chip_smooth = self.data_rate * BATCH_UPDATES * 2
|
||||
self.clock_sync = bulk_sensor.ClockSyncRegression(mcu, chip_smooth)
|
||||
self.clock_updater = bulk_sensor.ChipClockUpdater(self.clock_sync,
|
||||
BYTES_PER_SAMPLE)
|
||||
self.last_error_count = 0
|
||||
# Process messages in batches
|
||||
self.batch_bulk = bulk_sensor.BatchBulkHelper(
|
||||
self.printer, self._process_batch,
|
||||
self._start_measurements, self._finish_measurements, BATCH_UPDATES)
|
||||
self.name = config.get_name().split()[-1]
|
||||
wh = self.printer.lookup_object('webhooks')
|
||||
wh.register_mux_endpoint("mpu9250/dump_mpu9250", "sensor", self.name,
|
||||
self._handle_dump_mpu9250)
|
||||
hdr = ('time', 'x_acceleration', 'y_acceleration', 'z_acceleration')
|
||||
self.batch_bulk.add_mux_endpoint("mpu9250/dump_mpu9250", "sensor",
|
||||
self.name, {'header': hdr})
|
||||
def _build_config(self):
|
||||
cmdqueue = self.i2c.get_command_queue()
|
||||
self.mcu.add_config_cmd("config_mpu9250 oid=%d i2c_oid=%d"
|
||||
@@ -105,28 +106,25 @@ class MPU9250:
|
||||
def read_reg(self, reg):
|
||||
params = self.i2c.i2c_read([reg], 1)
|
||||
return bytearray(params['response'])[0]
|
||||
|
||||
def set_reg(self, reg, val, minclock=0):
|
||||
self.i2c.i2c_write([reg, val & 0xFF], minclock=minclock)
|
||||
|
||||
# Measurement collection
|
||||
def is_measuring(self):
|
||||
return self.query_rate > 0
|
||||
def _handle_mpu9250_data(self, params):
|
||||
with self.lock:
|
||||
self.raw_samples.append(params)
|
||||
def start_internal_client(self):
|
||||
aqh = adxl345.AccelQueryHelper(self.printer)
|
||||
self.batch_bulk.add_client(aqh.handle_batch)
|
||||
return aqh
|
||||
# Measurement decoding
|
||||
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
|
||||
last_sequence = self.clock_updater.get_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 = (params['sequence'] - last_sequence) & 0xffff
|
||||
seq_diff -= (seq_diff & 0x8000) << 1
|
||||
seq = last_sequence - seq_diff
|
||||
seq = last_sequence + seq_diff
|
||||
d = bytearray(params['data'])
|
||||
msg_cdiff = seq * SAMPLES_PER_BLOCK - chip_base
|
||||
|
||||
@@ -150,116 +148,66 @@ class MPU9250:
|
||||
return samples
|
||||
|
||||
def _update_clock(self, minclock=0):
|
||||
# Query current state
|
||||
for retry in range(5):
|
||||
params = self.query_mpu9250_status_cmd.send([self.oid],
|
||||
minclock=minclock)
|
||||
fifo = params['fifo'] & 0x1fff
|
||||
if fifo <= FIFO_SIZE:
|
||||
break
|
||||
else:
|
||||
raise self.printer.command_error("Unable to query mpu9250 fifo")
|
||||
mcu_clock = self.mcu.clock32_to_clock64(params['clock'])
|
||||
sequence = (self.last_sequence & ~0xffff) | params['next_sequence']
|
||||
if sequence < self.last_sequence:
|
||||
sequence += 0x10000
|
||||
self.last_sequence = sequence
|
||||
buffered = params['buffered']
|
||||
limit_count = (self.last_limit_count & ~0xffff) | params['limit_count']
|
||||
if limit_count < self.last_limit_count:
|
||||
limit_count += 0x10000
|
||||
self.last_limit_count = limit_count
|
||||
duration = params['query_ticks']
|
||||
if duration > self.max_query_duration:
|
||||
# Skip measurement as a high query time could skew clock tracking
|
||||
self.max_query_duration = max(2 * self.max_query_duration,
|
||||
self.mcu.seconds_to_clock(.000005))
|
||||
return
|
||||
self.max_query_duration = 2 * duration
|
||||
msg_count = (sequence * SAMPLES_PER_BLOCK
|
||||
+ buffered // BYTES_PER_SAMPLE + fifo)
|
||||
# The "chip clock" is the message counter plus .5 for average
|
||||
# inaccuracy of query responses and plus .5 for assumed offset
|
||||
# of mpu9250 hw processing time.
|
||||
chip_clock = msg_count + 1
|
||||
self.clock_sync.update(mcu_clock + duration // 2, chip_clock)
|
||||
params = self.query_mpu9250_status_cmd.send([self.oid],
|
||||
minclock=minclock)
|
||||
self.clock_updater.update_clock(params)
|
||||
# Start, stop, and process message batches
|
||||
def _start_measurements(self):
|
||||
if self.is_measuring():
|
||||
return
|
||||
# In case of miswiring, testing MPU9250 device ID prevents treating
|
||||
# noise or wrong signal as a correctly initialized device
|
||||
dev_id = self.read_reg(REG_DEVID)
|
||||
if dev_id != MPU9250_DEV_ID and dev_id != MPU6050_DEV_ID:
|
||||
if dev_id not in MPU_DEV_IDS.keys():
|
||||
raise self.printer.command_error(
|
||||
"Invalid mpu9250/mpu6050 id (got %x).\n"
|
||||
"Invalid mpu id (got %x).\n"
|
||||
"This is generally indicative of connection problems\n"
|
||||
"(e.g. faulty wiring) or a faulty chip."
|
||||
% (dev_id))
|
||||
else:
|
||||
logging.info("Found %s with id %x"% (MPU_DEV_IDS[dev_id], dev_id))
|
||||
# Setup chip in requested query rate
|
||||
self.set_reg(REG_PWR_MGMT_1, SET_PWR_MGMT_1_WAKE)
|
||||
self.set_reg(REG_PWR_MGMT_2, SET_PWR_MGMT_2_ACCEL_ON)
|
||||
time.sleep(20. / 1000) # wait for accelerometer chip wake up
|
||||
self.set_reg(REG_SMPLRT_DIV, SAMPLE_RATE_DIVS[self.data_rate])
|
||||
# Add 20ms pause for accelerometer chip wake up
|
||||
self.read_reg(REG_DEVID) # Dummy read to ensure queues flushed
|
||||
systime = self.printer.get_reactor().monotonic()
|
||||
next_time = self.mcu.estimated_print_time(systime) + 0.020
|
||||
self.set_reg(REG_SMPLRT_DIV, SAMPLE_RATE_DIVS[self.data_rate],
|
||||
minclock=self.mcu.print_time_to_clock(next_time))
|
||||
self.set_reg(REG_CONFIG, SET_CONFIG)
|
||||
self.set_reg(REG_ACCEL_CONFIG, SET_ACCEL_CONFIG)
|
||||
self.set_reg(REG_ACCEL_CONFIG2, SET_ACCEL_CONFIG2)
|
||||
|
||||
# Setup samples
|
||||
with self.lock:
|
||||
self.raw_samples = []
|
||||
# Start bulk reading
|
||||
self.bulk_queue.clear_samples()
|
||||
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(1. / self.data_rate)
|
||||
self.query_rate = self.data_rate
|
||||
rest_ticks = self.mcu.seconds_to_clock(4. / self.data_rate)
|
||||
self.query_mpu9250_cmd.send([self.oid, reqclock, rest_ticks],
|
||||
reqclock=reqclock)
|
||||
logging.info("MPU9250 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.clock_updater.note_start(reqclock)
|
||||
self._update_clock(minclock=reqclock)
|
||||
self.max_query_duration = 1 << 31
|
||||
self.clock_updater.clear_duration_filter()
|
||||
self.last_error_count = 0
|
||||
def _finish_measurements(self):
|
||||
if not self.is_measuring():
|
||||
return
|
||||
# Halt bulk reading
|
||||
params = self.query_mpu9250_end_cmd.send([self.oid, 0, 0])
|
||||
self.query_rate = 0
|
||||
with self.lock:
|
||||
self.raw_samples = []
|
||||
self.bulk_queue.clear_samples()
|
||||
logging.info("MPU9250 finished '%s' measurements", self.name)
|
||||
self.set_reg(REG_PWR_MGMT_1, SET_PWR_MGMT_1_SLEEP)
|
||||
self.set_reg(REG_PWR_MGMT_2, SET_PWR_MGMT_2_OFF)
|
||||
|
||||
# API interface
|
||||
def _api_update(self, eventtime):
|
||||
def _process_batch(self, eventtime):
|
||||
self._update_clock()
|
||||
with self.lock:
|
||||
raw_samples = self.raw_samples
|
||||
self.raw_samples = []
|
||||
raw_samples = self.bulk_queue.pull_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_mpu9250(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 adxl345.AccelQueryHelper(self.printer, cconn)
|
||||
'overflows': self.clock_updater.get_last_limit_count()}
|
||||
|
||||
def load_config(config):
|
||||
return MPU9250(config)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -221,9 +221,9 @@ class Palette2:
|
||||
def _wait_for_heartbeat(self):
|
||||
startTs = self.reactor.monotonic()
|
||||
currTs = startTs
|
||||
while self.heartbeat is None and self.heartbeat < (
|
||||
currTs - SETUP_TIMEOUT) and startTs > (
|
||||
currTs - SETUP_TIMEOUT):
|
||||
while self.heartbeat is None or (self.heartbeat < (
|
||||
currTs - SETUP_TIMEOUT) and startTs > (
|
||||
currTs - SETUP_TIMEOUT)):
|
||||
currTs = self.reactor.pause(currTs + 1.)
|
||||
|
||||
if self.heartbeat < (currTs - SETUP_TIMEOUT):
|
||||
@@ -401,7 +401,7 @@ class Palette2:
|
||||
try:
|
||||
fw = params[0][1:]
|
||||
logging.info(
|
||||
"Palette 2 firmware version %s detected" % os.fwalk)
|
||||
"Palette 2 firmware version %s detected" % fw)
|
||||
except (TypeError, IndexError):
|
||||
logging.error("Unable to parse firmware version")
|
||||
|
||||
@@ -544,13 +544,15 @@ class Palette2:
|
||||
self.cmd_Disconnect()
|
||||
return self.reactor.NEVER
|
||||
if len(raw_bytes):
|
||||
text_buffer = self.read_buffer + str(raw_bytes.decode())
|
||||
new_buffer = str(raw_bytes.decode(encoding='UTF-8',
|
||||
errors='ignore'))
|
||||
text_buffer = self.read_buffer + new_buffer
|
||||
while True:
|
||||
i = text_buffer.find("\n")
|
||||
if i >= 0:
|
||||
line = text_buffer[0:i+1]
|
||||
line = text_buffer[0:i + 1]
|
||||
self.read_queue.put(line.strip())
|
||||
text_buffer = text_buffer[i+1:]
|
||||
text_buffer = text_buffer[i + 1:]
|
||||
else:
|
||||
break
|
||||
self.read_buffer = text_buffer
|
||||
@@ -566,7 +568,7 @@ class Palette2:
|
||||
|
||||
heartbeat_strings = [COMMAND_HEARTBEAT, "Connection Okay"]
|
||||
if not any(x in text_line for x in heartbeat_strings):
|
||||
logging.debug("%0.3f P2 -> : %s" %(eventtime, text_line))
|
||||
logging.debug("%0.3f P2 -> : %s" % (eventtime, text_line))
|
||||
|
||||
# Received a heartbeat from the device
|
||||
if text_line == COMMAND_HEARTBEAT:
|
||||
@@ -581,7 +583,7 @@ class Palette2:
|
||||
self.write_queue.put(COMMAND_HEARTBEAT)
|
||||
eventtime = self.reactor.pause(eventtime + 5)
|
||||
if self.heartbeat and self.heartbeat < (
|
||||
eventtime - HEARTBEAT_TIMEOUT):
|
||||
eventtime - HEARTBEAT_TIMEOUT):
|
||||
logging.error(
|
||||
"P2 has not responded to heartbeat")
|
||||
if not self.is_printing or self.is_setup_complete:
|
||||
@@ -610,6 +612,7 @@ class Palette2:
|
||||
logging.error("Unable to communicate with the Palette 2")
|
||||
self.signal_disconnect = True
|
||||
return self.reactor.NEVER
|
||||
return eventtime + SERIAL_TIMER
|
||||
return eventtime + SERIAL_TIMER
|
||||
|
||||
def _run_Smart_Load(self, eventtime):
|
||||
@@ -621,7 +624,7 @@ class Palette2:
|
||||
idle_time = est_print_time - print_time
|
||||
if not lookahead_empty or idle_time < 0.5:
|
||||
return eventtime + \
|
||||
max(0., min(1., print_time - est_print_time))
|
||||
max(0., min(1., print_time - est_print_time))
|
||||
|
||||
extrude = abs(self.remaining_load_length)
|
||||
extrude = min(50, extrude / 2)
|
||||
@@ -646,5 +649,6 @@ class Palette2:
|
||||
status["ping"] = self.omega_pings[-1]
|
||||
return status
|
||||
|
||||
|
||||
def load_config(config):
|
||||
return Palette2(config)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -10,6 +10,11 @@ class PrintStats:
|
||||
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)
|
||||
def _update_filament_usage(self, eventtime):
|
||||
gc_status = self.gcode_move.get_status(eventtime)
|
||||
cur_epos = gc_status['position'].e
|
||||
@@ -59,6 +64,24 @@ class PrintStats:
|
||||
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"
|
||||
@@ -66,6 +89,8 @@ class PrintStats:
|
||||
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:
|
||||
@@ -86,7 +111,9 @@ class PrintStats:
|
||||
'print_duration': print_duration,
|
||||
'filament_used': self.filament_used,
|
||||
'state': self.state,
|
||||
'message': self.error_message
|
||||
'message': self.error_message,
|
||||
'info': {'total_layer': self.info_total_layer,
|
||||
'current_layer': self.info_current_layer}
|
||||
}
|
||||
|
||||
def load_config(config):
|
||||
|
||||
Binary file not shown.
@@ -12,6 +12,7 @@ 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).
|
||||
"""
|
||||
_NERVER = 9999999
|
||||
|
||||
class PrinterProbe:
|
||||
def __init__(self, config, mcu_probe):
|
||||
@@ -28,6 +29,7 @@ class PrinterProbe:
|
||||
self.last_state = False
|
||||
self.last_z_result = 0.
|
||||
self.gcode_move = self.printer.load_object(config, "gcode_move")
|
||||
self.probe_count = 0
|
||||
# Infer Z position to move to during a probe
|
||||
if config.has_section('stepper_z'):
|
||||
zconfig = config.getsection('stepper_z')
|
||||
@@ -41,13 +43,15 @@ class PrinterProbe:
|
||||
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'}
|
||||
atypes = {'median': 'median', 'average': 'average',"submaxmin":"submaxmin"}
|
||||
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)
|
||||
# vibrate
|
||||
self.vibrate = config.getint('vibrate', _NERVER)
|
||||
# Register z_virtual_endstop pin
|
||||
self.printer.lookup_object('pins').register_chip('probe', self)
|
||||
# Register homing event handlers
|
||||
@@ -127,12 +131,13 @@ class PrinterProbe:
|
||||
if "Timeout during endstop homing" in reason:
|
||||
reason += HINT_TIMEOUT
|
||||
raise self.printer.command_error(reason)
|
||||
# get z compensation from x_twist
|
||||
# x_twist module checks if it is enabled, returns 0 compensation if not
|
||||
x_twist_compensation = self.printer.lookup_object(
|
||||
'x_twist_compensation', None)
|
||||
z_compensation = 0 if not x_twist_compensation \
|
||||
else x_twist_compensation.get_z_compensation_value(pos[0])
|
||||
# get z compensation from axis_twist_compensation
|
||||
axis_twist_compensation = self.printer.lookup_object(
|
||||
'axis_twist_compensation', None)
|
||||
z_compensation = 0
|
||||
if axis_twist_compensation is not None:
|
||||
z_compensation = (
|
||||
axis_twist_compensation.get_z_compensation_value(pos))
|
||||
# add z compensation to probe position
|
||||
epos[2] += z_compensation
|
||||
self.gcode.respond_info("probe at %.3f,%.3f is z=%.6f"
|
||||
@@ -169,6 +174,9 @@ class PrinterProbe:
|
||||
probexy = self.printer.lookup_object('toolhead').get_position()[:2]
|
||||
retries = 0
|
||||
positions = []
|
||||
gcode = self.gcode
|
||||
last_probe_failed = False
|
||||
start_z = self.printer.lookup_object('toolhead').get_position()[2]
|
||||
while len(positions) < sample_count:
|
||||
# Probe position
|
||||
pos = self._probe(speed)
|
||||
@@ -177,18 +185,45 @@ class PrinterProbe:
|
||||
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")
|
||||
# raise gcmd.error("Probe samples exceed samples_tolerance")
|
||||
self._move(probexy + [start_z], lift_speed)
|
||||
commands = [
|
||||
'Z_VIBRATE',
|
||||
'G4 P500',
|
||||
]
|
||||
gcode._process_commands(commands, False)
|
||||
retries=0
|
||||
positions = []
|
||||
gcmd.respond_info("Probe samples exceed tolerance. Retrying...")
|
||||
last_probe_failed = True
|
||||
self.probe_count += 1
|
||||
retries += 1
|
||||
positions = []
|
||||
# Retract
|
||||
if len(positions) < sample_count:
|
||||
self._move(probexy + [pos[2] + sample_retract_dist], lift_speed)
|
||||
if last_probe_failed:
|
||||
if self.probe_count != 0 and self.probe_count % self.vibrate == 0:
|
||||
gcode.respond_info('Probe ' + str(self.probe_count) + " times, start vibrating")
|
||||
commands = [
|
||||
'G91',
|
||||
'G1 Z20 F300',
|
||||
'G90',
|
||||
'G1 Z10 F300',
|
||||
'Z_VIBRATE'
|
||||
]
|
||||
gcode._process_commands(commands, False)
|
||||
last_probe_failed = False
|
||||
if must_notify_multi_probe:
|
||||
self.multi_probe_end()
|
||||
# Calculate and return result
|
||||
if samples_result == 'median':
|
||||
return self._calc_median(positions)
|
||||
if samples_result == 'submaxmin':
|
||||
z_sorted = sorted(positions, key=(lambda p: p[2]))
|
||||
if len(positions)>=3:
|
||||
z_sorted=z_sorted[1:-1]
|
||||
return self._calc_mean(z_sorted)
|
||||
return self._calc_mean(positions)
|
||||
cmd_PROBE_help = "Probe Z-height at current XY position"
|
||||
def cmd_PROBE(self, gcmd):
|
||||
@@ -203,7 +238,8 @@ class PrinterProbe:
|
||||
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,
|
||||
return {'name': self.name,
|
||||
'last_query': self.last_state,
|
||||
'last_z_result': self.last_z_result,
|
||||
'x_offset': self.x_offset,
|
||||
'y_offset': self.y_offset,
|
||||
@@ -259,8 +295,7 @@ class PrinterProbe:
|
||||
"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,))
|
||||
configfile.set(self.name, 'z_offset', "%.3f" % (0.000,))
|
||||
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)
|
||||
@@ -274,8 +309,7 @@ class PrinterProbe:
|
||||
# Move the nozzle over the probe point
|
||||
curpos[0] += self.x_offset
|
||||
curpos[1] += self.y_offset
|
||||
#PwAddNew
|
||||
self._move(curpos,80.)
|
||||
self._move(curpos, self.speed)
|
||||
# Start manual probe
|
||||
manual_probe.ManualProbeHelper(self.printer, gcmd,
|
||||
self.probe_calibrate_finalize)
|
||||
@@ -310,6 +344,7 @@ class ProbeEndstopWrapper:
|
||||
ppins = self.printer.lookup_object('pins')
|
||||
pin = config.get('pin')
|
||||
pin_params = ppins.lookup_pin(pin, can_invert=True, can_pullup=True)
|
||||
self.pin_params=pin_params
|
||||
mcu = pin_params['chip']
|
||||
self.mcu_endstop = mcu.setup_pin('endstop', pin_params)
|
||||
self.printer.register_event_handler('klippy:mcu_identify',
|
||||
@@ -375,7 +410,8 @@ class ProbePointsHelper:
|
||||
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.)
|
||||
def_move_z = config.getfloat('horizontal_move_z', 5.)
|
||||
self.default_horizontal_move_z = def_move_z
|
||||
self.speed = config.getfloat('speed', 50., above=0.)
|
||||
self.use_offsets = False
|
||||
# Internal probing state
|
||||
@@ -421,6 +457,9 @@ class ProbePointsHelper:
|
||||
probe = self.printer.lookup_object('probe', None)
|
||||
method = gcmd.get('METHOD', 'automatic').lower()
|
||||
self.results = []
|
||||
def_move_z = self.default_horizontal_move_z
|
||||
self.horizontal_move_z = gcmd.get_float('HORIZONTAL_MOVE_Z',
|
||||
def_move_z)
|
||||
if probe is None or method != 'automatic':
|
||||
# Manual probe
|
||||
self.lift_speed = self.speed
|
||||
@@ -434,11 +473,23 @@ class ProbePointsHelper:
|
||||
raise gcmd.error("horizontal_move_z can't be less than"
|
||||
" probe's z_offset")
|
||||
probe.multi_probe_begin()
|
||||
probe.probe_count = 0
|
||||
while 1:
|
||||
done = self._move_next()
|
||||
if done:
|
||||
break
|
||||
if self.gcode.break_flag:
|
||||
break
|
||||
pos = probe.run_probe(gcmd)
|
||||
probe.probe_count += 1
|
||||
gcode = self.printer.lookup_object('gcode')
|
||||
if probe.vibrate and probe.probe_count % probe.vibrate == 0:
|
||||
commands = [
|
||||
'G90',
|
||||
'G1 Z'+ str(self.horizontal_move_z) + ' F300',
|
||||
'Z_VIBRATE'
|
||||
]
|
||||
gcode._process_commands(commands, False)
|
||||
self.results.append(pos)
|
||||
probe.multi_probe_end()
|
||||
def _manual_probe_start(self):
|
||||
|
||||
Binary file not shown.
Binary file not shown.
185
klippy/extras/pwm_tool.py
Normal file
185
klippy/extras/pwm_tool.py
Normal file
@@ -0,0 +1,185 @@
|
||||
# Queued PWM gpio output
|
||||
#
|
||||
# Copyright (C) 2017-2023 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import chelper
|
||||
|
||||
MAX_SCHEDULE_TIME = 5.0
|
||||
CLOCK_SYNC_EXTRA_TIME = 0.050
|
||||
|
||||
class error(Exception):
|
||||
pass
|
||||
|
||||
class MCU_queued_pwm:
|
||||
def __init__(self, pin_params):
|
||||
self._mcu = pin_params['chip']
|
||||
self._hardware_pwm = False
|
||||
self._cycle_time = 0.100
|
||||
self._max_duration = 2.
|
||||
self._oid = self._mcu.create_oid()
|
||||
ffi_main, ffi_lib = chelper.get_ffi()
|
||||
self._stepqueue = ffi_main.gc(ffi_lib.stepcompress_alloc(self._oid),
|
||||
ffi_lib.stepcompress_free)
|
||||
self._mcu.register_stepqueue(self._stepqueue)
|
||||
self._stepcompress_queue_mq_msg = ffi_lib.stepcompress_queue_mq_msg
|
||||
self._mcu.register_config_callback(self._build_config)
|
||||
self._pin = pin_params['pin']
|
||||
self._invert = pin_params['invert']
|
||||
self._start_value = self._shutdown_value = float(self._invert)
|
||||
self._last_clock = self._last_value = self._default_value = 0
|
||||
self._duration_ticks = 0
|
||||
self._pwm_max = 0.
|
||||
self._set_cmd_tag = None
|
||||
self._toolhead = None
|
||||
printer = self._mcu.get_printer()
|
||||
printer.register_event_handler("klippy:connect", self._handle_connect)
|
||||
def _handle_connect(self):
|
||||
self._toolhead = self._mcu.get_printer().lookup_object("toolhead")
|
||||
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):
|
||||
self._cycle_time = cycle_time
|
||||
self._hardware_pwm = hardware_pwm
|
||||
def setup_start_value(self, start_value, shutdown_value):
|
||||
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))
|
||||
def _build_config(self):
|
||||
config_error = self._mcu.get_printer().config_error
|
||||
if self._max_duration and self._start_value != self._shutdown_value:
|
||||
raise config_error("Pin with max duration must have start"
|
||||
" value equal to shutdown value")
|
||||
cmd_queue = self._mcu.alloc_command_queue()
|
||||
curtime = self._mcu.get_printer().get_reactor().monotonic()
|
||||
printtime = self._mcu.estimated_print_time(curtime)
|
||||
self._last_clock = self._mcu.print_time_to_clock(printtime + 0.200)
|
||||
cycle_ticks = self._mcu.seconds_to_clock(self._cycle_time)
|
||||
if cycle_ticks >= 1<<31:
|
||||
raise config_error("PWM pin cycle time too large")
|
||||
self._duration_ticks = self._mcu.seconds_to_clock(self._max_duration)
|
||||
if self._duration_ticks >= 1<<31:
|
||||
raise config_error("PWM pin max duration too large")
|
||||
if self._duration_ticks:
|
||||
self._mcu.register_flush_callback(self._flush_notification)
|
||||
if self._hardware_pwm:
|
||||
self._pwm_max = self._mcu.get_constant_float("PWM_MAX")
|
||||
self._default_value = self._shutdown_value * self._pwm_max
|
||||
self._mcu.add_config_cmd(
|
||||
"config_pwm_out oid=%d pin=%s cycle_ticks=%d value=%d"
|
||||
" default_value=%d max_duration=%d"
|
||||
% (self._oid, self._pin, cycle_ticks,
|
||||
self._start_value * self._pwm_max,
|
||||
self._default_value, self._duration_ticks))
|
||||
self._last_value = int(self._start_value * self._pwm_max + 0.5)
|
||||
self._mcu.add_config_cmd("queue_pwm_out oid=%d clock=%d value=%d"
|
||||
% (self._oid, self._last_clock,
|
||||
self._last_value),
|
||||
on_restart=True)
|
||||
self._set_cmd_tag = self._mcu.lookup_command(
|
||||
"queue_pwm_out oid=%c clock=%u value=%hu",
|
||||
cq=cmd_queue).get_command_tag()
|
||||
return
|
||||
# Software PWM
|
||||
if self._shutdown_value not in [0., 1.]:
|
||||
raise config_error("shutdown value must be 0.0 or 1.0 on soft pwm")
|
||||
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 >= 1.0,
|
||||
self._shutdown_value >= 0.5, self._duration_ticks))
|
||||
self._default_value = int(self._shutdown_value >= 0.5) * cycle_ticks
|
||||
self._mcu.add_config_cmd(
|
||||
"set_digital_out_pwm_cycle oid=%d cycle_ticks=%d"
|
||||
% (self._oid, cycle_ticks))
|
||||
self._pwm_max = float(cycle_ticks)
|
||||
self._last_value = int(self._start_value * self._pwm_max + 0.5)
|
||||
self._mcu.add_config_cmd(
|
||||
"queue_digital_out oid=%d clock=%d on_ticks=%d"
|
||||
% (self._oid, self._last_clock, self._last_value), is_init=True)
|
||||
self._set_cmd_tag = self._mcu.lookup_command(
|
||||
"queue_digital_out oid=%c clock=%u on_ticks=%u",
|
||||
cq=cmd_queue).get_command_tag()
|
||||
def _send_update(self, clock, val):
|
||||
self._last_clock = clock = max(self._last_clock, clock)
|
||||
self._last_value = val
|
||||
data = (self._set_cmd_tag, self._oid, clock & 0xffffffff, val)
|
||||
ret = self._stepcompress_queue_mq_msg(self._stepqueue, clock,
|
||||
data, len(data))
|
||||
if ret:
|
||||
raise error("Internal error in stepcompress")
|
||||
# Notify toolhead so that it will flush this update
|
||||
wakeclock = clock
|
||||
if self._last_value != self._default_value:
|
||||
# Continue flushing to resend time
|
||||
wakeclock += self._duration_ticks
|
||||
wake_print_time = self._mcu.clock_to_print_time(wakeclock)
|
||||
self._toolhead.note_kinematic_activity(wake_print_time
|
||||
+ CLOCK_SYNC_EXTRA_TIME)
|
||||
def set_pwm(self, print_time, value):
|
||||
clock = self._mcu.print_time_to_clock(print_time)
|
||||
if self._invert:
|
||||
value = 1. - value
|
||||
v = int(max(0., min(1., value)) * self._pwm_max + 0.5)
|
||||
self._send_update(clock, v)
|
||||
def _flush_notification(self, print_time, clock):
|
||||
if self._last_value != self._default_value:
|
||||
while clock >= self._last_clock + self._duration_ticks:
|
||||
self._send_update(self._last_clock + self._duration_ticks,
|
||||
self._last_value)
|
||||
|
||||
class PrinterOutputPin:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
ppins = self.printer.lookup_object('pins')
|
||||
# Determine pin type
|
||||
pin_params = ppins.lookup_pin(config.get('pin'), can_invert=True)
|
||||
self.mcu_pin = MCU_queued_pwm(pin_params)
|
||||
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_print_time = 0.
|
||||
# Support mcu checking for maximum duration
|
||||
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)
|
||||
# Determine start and shutdown values
|
||||
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)
|
||||
# Register commands
|
||||
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)
|
||||
def get_status(self, eventtime):
|
||||
return {'value': self.last_value}
|
||||
def _set_pin(self, print_time, value):
|
||||
if value == self.last_value:
|
||||
return
|
||||
print_time = max(print_time, self.last_print_time)
|
||||
self.mcu_pin.set_pwm(print_time, value)
|
||||
self.last_value = value
|
||||
self.last_print_time = print_time
|
||||
cmd_SET_PIN_help = "Set the value of an output pin"
|
||||
def cmd_SET_PIN(self, gcmd):
|
||||
# Read requested value
|
||||
value = gcmd.get_float('VALUE', minval=0., maxval=self.scale)
|
||||
value /= self.scale
|
||||
# Obtain print_time and apply requested settings
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
toolhead.register_lookahead_callback(
|
||||
lambda print_time: self._set_pin(print_time, value))
|
||||
|
||||
def load_config_prefix(config):
|
||||
return PrinterOutputPin(config)
|
||||
135
klippy/extras/qdprobe.py
Normal file
135
klippy/extras/qdprobe.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import logging
|
||||
from . import probe
|
||||
|
||||
# Makerbase Endstop wrapper
|
||||
class MakerbaseProbeEndstopWrapper:
|
||||
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', '')
|
||||
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
|
||||
|
||||
class MakerBase:
|
||||
def __init__(self, config):
|
||||
self.printer = config.get_printer()
|
||||
self.gcode = self.printer.lookup_object('gcode')
|
||||
# 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.probe = self.printer.lookup_object('probe')
|
||||
self.endstop_wrapper = probe.ProbeEndstopWrapper(config)
|
||||
# self.probe_accel = config.getfloat('probe_accel', 0., minval=0.)
|
||||
# self.recovery_time = config.getfloat('recovery_time', 0.4, minval=0.)
|
||||
# Register MakerBase commands
|
||||
self.gcode.register_command('MKS_PROBE_PIN_1', self.cmd_MKS_PROBE_PIN_1,
|
||||
desc=self.cmd_MKS_PROBE_PIN_1_help)
|
||||
self.gcode.register_command('MKS_PROBE_PIN_2', self.cmd_MKS_PROBE_PIN_2,
|
||||
desc=self.cmd_MKS_PROBE_PIN_2_help)
|
||||
self.gcode.register_command('QIDI_PROBE_PIN_1', self.cmd_MKS_PROBE_PIN_1,
|
||||
desc=self.cmd_MKS_PROBE_PIN_1_help)
|
||||
self.gcode.register_command('QIDI_PROBE_PIN_2', self.cmd_MKS_PROBE_PIN_2,
|
||||
desc=self.cmd_MKS_PROBE_PIN_2_help)
|
||||
self.gcode.register_command('MKS_REMOVE', self.cmd_MKS_REMOVE,
|
||||
desc=self.cmd_MKS_REMOVE_help)
|
||||
cmd_MKS_PROBE_PIN_2_help = 'ENABLE_PROBE_PIN_2'
|
||||
def cmd_MKS_PROBE_PIN_2(self, gcmd):
|
||||
self.probe.mcu_probe.probe_wrapper = self.endstop_wrapper
|
||||
# Wrappers
|
||||
self.probe.mcu_probe.get_mcu = self.endstop_wrapper.get_mcu
|
||||
self.probe.mcu_probe.add_stepper = self.endstop_wrapper.add_stepper
|
||||
self.probe.mcu_probe.get_steppers = self.endstop_wrapper.get_steppers
|
||||
self.probe.mcu_probe.home_start = self.endstop_wrapper.home_start
|
||||
self.probe.mcu_probe.home_wait = self.endstop_wrapper.home_wait
|
||||
self.probe.mcu_probe.query_endstop = self.endstop_wrapper.query_endstop
|
||||
self.probe.mcu_probe.multi_probe_begin = self.endstop_wrapper.multi_probe_begin
|
||||
self.probe.mcu_probe.multi_probe_end = self.endstop_wrapper.multi_probe_end
|
||||
# gcmd.respond_raw("%s" % (self.cmd_MKS_PROBE_PIN_2_help, ))
|
||||
cmd_MKS_REMOVE_help = 'MKS_REMOVE'
|
||||
def cmd_MKS_REMOVE(self, gcmd):
|
||||
self.printer.remove_object('probe')
|
||||
self.printer.lookup_object('gcode').remove_command('PROBE')
|
||||
self.printer.lookup_object('gcode').remove_command('QUERY_PROBE')
|
||||
self.printer.lookup_object('gcode').remove_command('PROBE_CALIBRATE')
|
||||
self.printer.lookup_object('gcode').remove_command('PROBE_ACCURACY')
|
||||
self.printer.lookup_object('gcode').remove_command('Z_OFFSET_APPLY_PROBE')
|
||||
self.printer.lookup_object('gcode').remove_command('MKS_SHOW_Z_OFFSET')
|
||||
self.printer.lookup_object('pins').remove_chip('probe')
|
||||
cmd_MKS_PROBE_PIN_1_help = 'ENABLE_PROBE_PIN_1'
|
||||
def cmd_MKS_PROBE_PIN_1(self, gcmd):
|
||||
self.probe.mcu_probe.probe_wrapper = self.probe.mcu_probe.probe_wrapper_2
|
||||
# Wrappers
|
||||
self.probe.mcu_probe.get_mcu = self.probe.mcu_probe.probe_wrapper_2.get_mcu
|
||||
self.probe.mcu_probe.add_stepper = self.probe.mcu_probe.probe_wrapper_2.add_stepper
|
||||
self.probe.mcu_probe.get_steppers = self.probe.mcu_probe.probe_wrapper_2.get_steppers
|
||||
self.probe.mcu_probe.home_start = self.probe.mcu_probe.probe_wrapper_2.home_start
|
||||
self.probe.mcu_probe.home_wait = self.probe.mcu_probe.probe_wrapper_2.home_wait
|
||||
self.probe.mcu_probe.query_endstop = self.probe.mcu_probe.probe_wrapper_2.query_endstop
|
||||
self.probe.mcu_probe.multi_probe_begin = self.probe.mcu_probe.probe_wrapper_2.multi_probe_begin
|
||||
self.probe.mcu_probe.multi_probe_end = self.probe.mcu_probe.probe_wrapper_2.multi_probe_end
|
||||
# gcmd.respond_raw("%s" % (self.cmd_MKS_PROBE_PIN_1_help, ))
|
||||
|
||||
def load_config(config):
|
||||
return MakerBase(config)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,7 @@ class pca9685_pwm:
|
||||
raise pins.error("Pin type not supported on replicape")
|
||||
self._mcu = replicape.host_mcu
|
||||
self._mcu.register_config_callback(self._build_config)
|
||||
self._reactor = self._mcu.get_printer().get_reactor()
|
||||
self._bus = REPLICAPE_PCA9685_BUS
|
||||
self._address = REPLICAPE_PCA9685_ADDRESS
|
||||
self._cycle_time = REPLICAPE_PCA9685_CYCLE_TIME
|
||||
@@ -28,6 +29,7 @@ class pca9685_pwm:
|
||||
self._oid = None
|
||||
self._invert = pin_params['invert']
|
||||
self._start_value = self._shutdown_value = float(self._invert)
|
||||
self._is_enable = not not self._start_value
|
||||
self._is_static = False
|
||||
self._last_clock = 0
|
||||
self._pwm_max = 0.
|
||||
@@ -53,6 +55,7 @@ class pca9685_pwm:
|
||||
self._is_static = is_static
|
||||
self._replicape.note_pwm_start_value(
|
||||
self._channel, self._start_value, self._shutdown_value)
|
||||
self._is_enable = not not self._start_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)
|
||||
@@ -80,7 +83,12 @@ class pca9685_pwm:
|
||||
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)
|
||||
is_enable = not not value
|
||||
if is_enable != self._is_enable:
|
||||
self._is_enable = is_enable
|
||||
self._reactor.register_async_callback(
|
||||
(lambda e, s=self, pt=print_time, ie=is_enable:
|
||||
s._replicape.note_pwm_enable(pt, s._channel, ie)))
|
||||
self._set_cmd.send([self._oid, clock, value],
|
||||
minclock=self._last_clock, reqclock=clock)
|
||||
self._last_clock = clock
|
||||
@@ -148,7 +156,7 @@ class servo_pwm:
|
||||
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);
|
||||
self.mcu_pwm.setup_cycle_time(cycle_time, True)
|
||||
|
||||
ReplicapeStepConfig = {
|
||||
'disable': None,
|
||||
@@ -171,6 +179,7 @@ class Replicape:
|
||||
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
|
||||
self.last_pwm_enable_time = 0.
|
||||
# Setup power pins
|
||||
self.pins = {
|
||||
"power_e": (pca9685_pwm, 5), "power_h": (pca9685_pwm, 3),
|
||||
@@ -180,7 +189,6 @@ class Replicape:
|
||||
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'):
|
||||
@@ -227,18 +235,17 @@ class Replicape:
|
||||
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
|
||||
def note_pwm_enable(self, print_time, channel, is_enable):
|
||||
self.enabled_channels[channel] = is_enable
|
||||
# Check if need to set the pca9685 enable pin
|
||||
pe_time = max(print_time, self.last_pwm_enable_time + PIN_MIN_TIME)
|
||||
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)
|
||||
self.mcu_pwm_enable.set_digital(pe_time, 0)
|
||||
self.last_pwm_enable_time = pe_time
|
||||
elif is_enable and len(on_channels) == 1:
|
||||
self.mcu_pwm_enable.set_digital(print_time, 1)
|
||||
self.mcu_pwm_enable.set_digital(pe_time, 1)
|
||||
self.last_pwm_enable_time = pe_time
|
||||
# Check if need to set the stepper enable lines
|
||||
if channel not in self.stepper_dacs:
|
||||
return
|
||||
@@ -250,7 +257,6 @@ class Replicape:
|
||||
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):
|
||||
|
||||
@@ -207,11 +207,21 @@ class ResonanceTester:
|
||||
else:
|
||||
calibration_data[axis].add_data(new_data)
|
||||
return calibration_data
|
||||
def _parse_chips(self, 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)
|
||||
return parsed_chips
|
||||
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)
|
||||
chips_str = gcmd.get("CHIPS", None)
|
||||
test_point = gcmd.get("POINT", None)
|
||||
|
||||
if test_point:
|
||||
@@ -224,15 +234,7 @@ class ResonanceTester:
|
||||
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)
|
||||
accel_chips = self._parse_chips(chips_str) if chips_str else None
|
||||
|
||||
outputs = gcmd.get("OUTPUT", "resonances").lower().split(',')
|
||||
for output in outputs:
|
||||
@@ -257,8 +259,7 @@ class ResonanceTester:
|
||||
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]
|
||||
accel_chips=accel_chips, test_point=test_point)[axis]
|
||||
if csv_output:
|
||||
csv_name = self.save_calibration_data('resonances', name_suffix,
|
||||
helper, axis, data,
|
||||
@@ -276,6 +277,8 @@ class ResonanceTester:
|
||||
raise gcmd.error("Unsupported axis '%s'" % (axis,))
|
||||
else:
|
||||
calibrate_axes = [TestAxis(axis.lower())]
|
||||
chips_str = gcmd.get("CHIPS", None)
|
||||
accel_chips = self._parse_chips(chips_str) if chips_str else None
|
||||
|
||||
max_smoothing = gcmd.get_float(
|
||||
"MAX_SMOOTHING", self.max_smoothing, minval=0.05)
|
||||
@@ -284,10 +287,13 @@ class ResonanceTester:
|
||||
if not self.is_valid_name_suffix(name_suffix):
|
||||
raise gcmd.error("Invalid NAME parameter")
|
||||
|
||||
input_shaper = self.printer.lookup_object('input_shaper', None)
|
||||
|
||||
# Setup shaper calibration
|
||||
helper = shaper_calibrate.ShaperCalibrate(self.printer)
|
||||
|
||||
calibration_data = self._run_test(gcmd, calibrate_axes, helper)
|
||||
calibration_data = self._run_test(gcmd, calibrate_axes, helper,
|
||||
accel_chips=accel_chips)
|
||||
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
for axis in calibrate_axes:
|
||||
@@ -302,6 +308,9 @@ class ResonanceTester:
|
||||
"Recommended shaper_type_%s = %s, shaper_freq_%s = %.1f Hz"
|
||||
% (axis_name, best_shaper.name,
|
||||
axis_name, best_shaper.freq))
|
||||
if input_shaper is not None:
|
||||
helper.apply_params(input_shaper, axis_name,
|
||||
best_shaper.name, best_shaper.freq)
|
||||
helper.save_params(configfile, axis_name,
|
||||
best_shaper.name, best_shaper.freq)
|
||||
csv_name = self.save_calibration_data(
|
||||
|
||||
Binary file not shown.
@@ -79,7 +79,10 @@ class SafeZHoming:
|
||||
self.prev_G28(g28_gcmd)
|
||||
# Perform Z Hop again for pressure-based probes
|
||||
if self.z_hop:
|
||||
toolhead.manual_move([None, None, self.z_hop], self.z_hop_speed)
|
||||
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)
|
||||
|
||||
Binary file not shown.
@@ -12,12 +12,37 @@ class SaveVariables:
|
||||
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 load_variable(self, section, option):
|
||||
varfile = configparser.ConfigParser()
|
||||
try:
|
||||
varfile.read(self.filename)
|
||||
return varfile.get(section, option)
|
||||
except:
|
||||
msg = "Unable to parse existing variable file"
|
||||
logging.exception(msg)
|
||||
raise self.printer.command_error(msg)
|
||||
def save_variable(self, section, option, value):
|
||||
varfile = configparser.ConfigParser()
|
||||
try:
|
||||
varfile.read(self.filename)
|
||||
if not varfile.has_section(section):
|
||||
varfile.add_section(section)
|
||||
varfile.set(section, option, value)
|
||||
with open(self.filename, 'w') as configfile:
|
||||
varfile.write(configfile)
|
||||
except Exception as e:
|
||||
msg = "Unable to save variable"
|
||||
logging.exception(msg)
|
||||
raise self.printer.command_error(msg)
|
||||
self.loadVariables()
|
||||
def loadVariables(self):
|
||||
allvars = {}
|
||||
varfile = configparser.ConfigParser()
|
||||
@@ -54,7 +79,6 @@ class SaveVariables:
|
||||
msg = "Unable to save variable"
|
||||
logging.exception(msg)
|
||||
raise gcmd.error(msg)
|
||||
gcmd.respond_info("Variable Saved")
|
||||
self.loadVariables()
|
||||
def get_status(self, eventtime):
|
||||
return {'variables': self.allVariables}
|
||||
|
||||
@@ -12,7 +12,9 @@ class ScrewsTiltAdjust:
|
||||
self.config = config
|
||||
self.printer = config.get_printer()
|
||||
self.screws = []
|
||||
self.results = []
|
||||
self.max_diff = None
|
||||
self.max_diff_error = False
|
||||
# Read config
|
||||
for i in range(99):
|
||||
prefix = "screw%d" % (i + 1,)
|
||||
@@ -26,7 +28,7 @@ class ScrewsTiltAdjust:
|
||||
raise config.error("screws_tilt_adjust: Must have "
|
||||
"at least three screws")
|
||||
self.threads = {'CW-M3': 0, 'CCW-M3': 1, 'CW-M4': 2, 'CCW-M4': 3,
|
||||
'CW-M5': 4, 'CCW-M5': 5}
|
||||
'CW-M5': 4, 'CCW-M5': 5, 'CW-M6': 6, 'CCW-M6': 7}
|
||||
self.thread = config.getchoice('screw_thread', self.threads,
|
||||
default='CW-M3')
|
||||
# Initialize ProbePointsHelper
|
||||
@@ -57,9 +59,18 @@ class ScrewsTiltAdjust:
|
||||
self.direction = direction
|
||||
self.probe_helper.start_probe(gcmd)
|
||||
|
||||
def get_status(self, eventtime):
|
||||
return {'error': self.max_diff_error,
|
||||
'max_deviation': self.max_diff,
|
||||
'results': self.results}
|
||||
|
||||
def probe_finalize(self, offsets, positions):
|
||||
# Factors used for CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5 and CCW-M5
|
||||
threads_factor = {0: 0.5, 1: 0.5, 2: 0.7, 3: 0.7, 4: 0.8, 5: 0.8}
|
||||
self.results = {}
|
||||
self.max_diff_error = False
|
||||
# Factors used for CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, CW-M6
|
||||
#and CCW-M6
|
||||
threads_factor = {0: 0.5, 1: 0.5, 2: 0.7, 3: 0.7, 4: 0.8, 5: 0.8,
|
||||
6: 1.0, 7: 1.0}
|
||||
is_clockwise_thread = (self.thread & 1) == 0
|
||||
screw_diff = []
|
||||
# Process the read Z values
|
||||
@@ -84,6 +95,9 @@ class ScrewsTiltAdjust:
|
||||
self.gcode.respond_info(
|
||||
"%s : x=%.1f, y=%.1f, z=%.5f" %
|
||||
(name + ' (base)', coord[0], coord[1], z))
|
||||
sign = "CW" if is_clockwise_thread else "CCW"
|
||||
self.results["screw%d" % (i + 1,)] = {'z': z, 'sign': sign,
|
||||
'adjust': '00:00', 'is_base': True}
|
||||
else:
|
||||
# Calculate how knob must be adjusted for other positions
|
||||
diff = z_base - z
|
||||
@@ -104,7 +118,11 @@ class ScrewsTiltAdjust:
|
||||
self.gcode.respond_info(
|
||||
"%s : x=%.1f, y=%.1f, z=%.5f : adjust %s %02d:%02d" %
|
||||
(name, coord[0], coord[1], z, sign, full_turns, minutes))
|
||||
self.results["screw%d" % (i + 1,)] = {'z': z, 'sign': sign,
|
||||
'adjust':"%02d:%02d" % (full_turns, minutes),
|
||||
'is_base': False}
|
||||
if self.max_diff and any((d > self.max_diff) for d in screw_diff):
|
||||
self.max_diff_error = True
|
||||
raise self.gcode.error(
|
||||
"bed level exceeds configured limits ({}mm)! " \
|
||||
"Adjust screws and restart print.".format(self.max_diff))
|
||||
|
||||
@@ -334,6 +334,18 @@ class ShaperCalibrate:
|
||||
configfile.set('input_shaper', 'shaper_freq_'+axis,
|
||||
'%.1f' % (shaper_freq,))
|
||||
|
||||
def apply_params(self, input_shaper, axis, shaper_name, shaper_freq):
|
||||
if axis == 'xy':
|
||||
self.apply_params(input_shaper, 'x', shaper_name, shaper_freq)
|
||||
self.apply_params(input_shaper, 'y', shaper_name, shaper_freq)
|
||||
return
|
||||
gcode = self.printer.lookup_object("gcode")
|
||||
axis = axis.upper()
|
||||
input_shaper.cmd_SET_INPUT_SHAPER(gcode.create_gcode_command(
|
||||
"SET_INPUT_SHAPER", "SET_INPUT_SHAPER", {
|
||||
"SHAPER_TYPE_" + axis: shaper_name,
|
||||
"SHAPER_FREQ_" + axis: shaper_freq}))
|
||||
|
||||
def save_calibration_data(self, output, calibration_data, shapers=None):
|
||||
try:
|
||||
with open(output, "w") as csvfile:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user