Merge remote-tracking branch 'upstream/master' into dev

This commit is contained in:
Desuuuu 2020-09-19 14:14:23 +02:00
commit ccf58ec4bf
No known key found for this signature in database
GPG Key ID: 85943F4B2C2CE0DC
66 changed files with 1974 additions and 614 deletions

View File

@ -556,6 +556,32 @@
# measurements, e.g. with an accelerometer.
# Default value is 0.1 which is a good all-round value for most printers.
# Support for ADXL345 accelerometers. This support allows one to query
# accelerometer measurements from the sensor. This enables an
# ACCELEROMETER_MEASURE command (see docs/G-Codes.md for more
# information). The default chip name is "default", but one may
# specify an explicit name (eg, [adxl345 my_chip_name]).
#[adxl345]
#axes_map: x,y,z
# The accelerometer axis for each of the printer's x, y, and z axes.
# This may be useful if the accelerometer is mounted in an
# orientation that does not match the printer orientation. For
# example, one could set this to "y,x,z" to swap the x and y axes.
# It is also possible to negate an axis if the accelerometer
# direction is reversed (eg, "x,z,-y"). The default is "x,y,z".
#cs_pin:
# The SPI enable pin for the sensor. This parameter must be
# provided.
#spi_speed: 5000000
# The SPI speed (in hz) to use when communicating with the chip.
# The default is 5000000.
#spi_bus:
#spi_software_sclk_pin:
#spi_software_mosi_pin:
#spi_software_miso_pin:
# These optional parameters allow one to customize the SPI settings
# used to communicate with the chip.
######################################################################
# Config file helpers
@ -1304,6 +1330,14 @@
#pin:
# The pin on which the button is connected. This parameter must be
# provided.
#analog_range:
# Two comma separated resistances (in Ohms) specifying the minimum
# and maximum resistance range for the button. If analog_range is
# provided then the pin must be an analog capable pin. The default
# is to use digital gpio for the button.
#analog_pullup_resistor:
# The pullup resistance (in Ohms) when analog_range is specified.
# The default is 4700 ohms.
#press_gcode:
# A list of G-Code commands to execute when the button is pressed.
# G-Code templates are supported. This parameter must be provided.
@ -1867,6 +1901,8 @@
# Set the Vcomh value on SSD1306/SH1106 displays. This value is
# associated with a "smearing" effect on some OLED displays.
# The value may range from 0 to 63. Default is 0.
#x_offset: 0
# Set the horizontal offset value on SSD1306/SH1106 displays. Default is 0.
#invert: FALSE
# TRUE inverts the pixels on certain OLED (SSD1306/SH1106) displays
# The default is FALSE
@ -2188,6 +2224,23 @@
# communication. The pin must have a valid pinmux configuration
# for the given SERCOM peripheral. This parameter must be provided.
# Duet2 Maestro analog scaling by vref and vssa readings. Defining an
# adc_scaled section enables virtual adc pins (such as "my_name:PB0")
# that are automatically adjusted by the board's vref and vssa
# monitoring pins. Be sure to define this config section above any
# config sections that use one these virtual pins.
#[adc_scaled my_name]
#vref_pin:
# The ADC pin to use for VREF monitoring. This parameter must be
# provided.
#vssa_pin:
# The ADC pin to use for VSSA monitoring. This parameter must be
# provided.
#smooth_time: 2.0
# A time value (in seconds) over which the vref and vssa
# measurements will be smoothed to reduce the impact of measurement
# noise. The default is 2 seconds.
# Replicape support - see the generic-replicape.cfg file for further
# details.
#[replicape]

View File

@ -59,6 +59,11 @@ microsteps: 16
run_current: 0.800
stealthchop_threshold: 30
# Support analog sensor adjustments using VREF/VSSA pins
[adc_scaled vref_scaled]
vref_pin: PA17
vssa_pin: PA19
[extruder]
step_pin: PC4
dir_pin: PB7
@ -69,7 +74,7 @@ filament_diameter: 1.750
heater_pin: !PC1
sensor_type: EPCOS 100K B57560G104F
pullup_resistor: 2200
sensor_pin: PB0
sensor_pin: vref_scaled:PB0
control: pid
pid_Kp: 22.2
pid_Ki: 1.08
@ -110,7 +115,7 @@ stealthchop_threshold: 5
heater_pin: !PC0
sensor_type: EPCOS 100K B57560G104F
pullup_resistor: 2200
sensor_pin: PA20
sensor_pin: vref_scaled:PA20
control: watermark
min_temp: 0
max_temp: 130

View File

@ -134,3 +134,31 @@ pin: replicape:power_fan0
# PWM output pin controlling the servo. This parameter must be
# provided.
#...
# Providing an example of a switch filament sensor using the Linux MCU for replicape, instead of the PRU which does not have enough memory:
#[filament_switch_sensor switch_sensor]
#switch_pin: HOST_X2_STOP
# providing board pin aliases
[board_pins]
aliases:
# step/dir pins
X_DIR=P8_26, X_STEP=P8_17, Y_DIR=P8_19, Y_STEP=P8_12, Z_DIR=P8_14, Z_STEP=P8_13,
E_DIR=P8_15, E_STEP=P9_12, H_DIR=P8_16, H_STEP=P8_11,
# stepper fault pins
FAULT_X=P8_10, FAULT_Y=P8_9, FAULT_Z=P9_24, FAULT_E=P8_18, FAULT_H=P8_8,
# endstops
STOP_X1=P9_25, STOP_X2=P9_11, STOP_Y1=P9_23, STOP_Y2=P9_28, STOP_Z1=P9_13, STOP_Z2=P9_18,
# enable steppers (all on one pin)
STEPPER_ENABLE=P9_41,
# servos
SERVO_0=P9_14, SERVO_1=P9_16,
# Thermistors
THERM_E=P9_33, THERM_H=P9_36, THERM_BED=P9_35,
# D1W pin
DALLAS=P9_22
[board_pins host]
aliases:
# Host aliases for Linux MCU
HOST_X2_STOP=gpio30, HOST_Y2_STOP=gpio113, HOST_Z2_STOP=gpio4

View File

@ -0,0 +1,103 @@
# This file contains pin mappings for the stock 2020 Creality Ender 3
# Pro with the 32-bit Creality 4.2.2 board. To use this config, during
# "make menuconfig" select the STM32F103 with a "28KiB bootloader" and
# with "Use USB for communication" disabled.
# If you prefer a direct serial connection, in "make menuconfig"
# select "Enable extra low-level configuration options" and select the
# USART3 serial port, which is broken out on the 10 pin IDC cable used
# for the LCD module as follows:
# 3: Tx, 4: Rx, 9: GND, 10: VCC
# Flash this firmware by copying "out/klipper.bin" to a SD card and
# turning on the printer with the card inserted. The firmware
# filename must end in ".bin" and must not match the last filename
# that was flashed.
# See the example.cfg file for a description of available parameters.
[stepper_x]
step_pin: PC2
dir_pin: PB9
enable_pin: !PC3
step_distance: .0125
endstop_pin: ^PA5
position_endstop: 0
position_max: 235
homing_speed: 50
[stepper_y]
step_pin: PB8
dir_pin: PB7
enable_pin: !PC3
step_distance: .0125
endstop_pin: ^PA6
position_endstop: 0
position_max: 235
homing_speed: 50
[stepper_z]
step_pin: PB6
dir_pin: !PB5
enable_pin: !PC3
step_distance: .0025
endstop_pin: ^PA7
position_endstop: 0.0
position_max: 250
[extruder]
max_extrude_only_distance: 100.0
step_pin: PB4
dir_pin: PB3
enable_pin: !PC3
step_distance: 0.010752
nozzle_diameter: 0.400
filament_diameter: 1.750
heater_pin: PA1
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PC5
control: pid
# tuned for stock hardware with 200 degree Celsius target
pid_Kp: 21.527
pid_Ki: 1.063
pid_Kd: 108.982
min_temp: 0
max_temp: 250
[heater_bed]
heater_pin: PA2
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PC4
control: pid
# tuned for stock hardware with 50 degree Celsius target
pid_Kp: 54.027
pid_Ki: 0.770
pid_Kd: 948.182
min_temp: 0
max_temp: 130
[fan]
pin: PA0
[mcu]
serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0
[printer]
kinematics: cartesian
max_velocity: 300
max_accel: 3000
max_z_velocity: 5
max_z_accel: 100
# Pin mappings for BL_T port
#[bltouch]
#sensor_pin: ^PB1
#control_pin: PB0
[display]
lcd_type: st7920
cs_pin: PB12
sclk_pin: PB13
sid_pin: PB15
encoder_pins: ^PB14, ^PB10
click_pin: ^!PB2

View File

@ -0,0 +1,136 @@
# This is a Klipper configuration for TronXY X5SA, with
# CXY-V6 motherboard.
# === FLASHING WITH STOCK BOOTLOADER ===
# You should make firmware for STM32F103 with bootloader offset
# at 0x8008800 (Chitu v6 Bootloader). Uncheck USB, and leave default
# serial settings.
#
# Use "./update_chitu.py out/klipper.bin update.cbd" to generate update.cbd.
# Put `update.cbd` onto SD card, and reboot the printer.
# It will be automatically installed, and you will be able to update it this way.
[mcu]
serial: /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0
restart_method: command
[printer]
kinematics: corexy
max_velocity: 300
max_accel: 3000
max_z_velocity: 25
max_z_accel: 30
[stepper_x]
step_pin: PE5
dir_pin: !PE6
enable_pin: !PC13
step_distance: .006275
endstop_pin: !PG10
position_endstop: -1
position_min: -1
position_max: 350
homing_speed: 50
homing_retract_dist: 10
second_homing_speed: 10.0
[stepper_y]
step_pin: PE2
dir_pin: !PE3
enable_pin: !PE4
step_distance: .006275
endstop_pin: !PA12
position_endstop: 0
position_max: 330
homing_retract_dist: 10
homing_speed: 50.0
second_homing_speed: 10.0
[stepper_z]
step_pin: PB9
dir_pin: PE0
enable_pin: !PE1
step_distance: .00125
endstop_pin: probe:z_virtual_endstop
position_max: 400
position_min: -2
[extruder]
step_pin: PB4
dir_pin: PB5
enable_pin: !PB8
step_distance: 0.0111
nozzle_diameter: 0.400
filament_diameter: 1.750
heater_pin: PG12
sensor_type: ATC Semitec 104GT-2
sensor_pin: PA1
control: pid
pid_Kp: 18.831
pid_Ki: 0.821
pid_Kd: 108.044
min_temp: 0
max_temp: 250
max_extrude_only_distance: 300
[heater_bed]
heater_pin: PG11
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PA0
control: pid
min_temp: 0
max_temp: 130
pid_Kp: 73.932
pid_Ki: 1.521
pid_Kd: 898.279
[heater_fan hotend_fan]
pin: PG14
fan_speed: 0.5
[fan]
pin: PG13
max_power: 0.5
[controller_fan drivers_fan]
pin: PD6
[filament_switch_sensor sentinel]
pause_on_runout: True
runout_gcode:
M25
switch_pin: PA15
[output_pin beeper]
pin: PB0
[safe_z_home]
home_xy_position: 165,165
speed: 50
z_hop: 10
z_hop_speed: 5
[bed_screws]
screw1: 5,5
screw2: 165,5
screw3: 325,5
screw4: 5,325
screw5: 165,325
screw6: 325,325
[bed_mesh]
speed: 120
probe_count: 5,5
horizontal_move_z: 5
algorithm: lagrange
mesh_min : 20,20
mesh_max : 310,310
mesh_pps: 0
[probe]
x_offset: -40
y_offset: 0
pin: !PG9
speed: 30
z_offset: 2

View File

@ -295,13 +295,30 @@ the coordinates listed under the "Probe" header to find the correct index.
### Calibration
`BED_MESH_CALIBRATE METHOD=[manual | automatic]`\
`BED_MESH_CALIBRATE METHOD=[manual | automatic] [<probe_parameter>=<value>]
[<mesh_parameter>=<value>]`\
_Default Method: automatic if a probe is detected, otherwise manual_
Initiates the probing procedure for Bed Mesh Calibration. If `METHOD=manual`
is selected then manual probing will occur. When switching between automatic
and manual probing the generated mesh points will automatically be adjusted.
It is possible to specify mesh parameters to modify the probed area. The
following parameters are available:
- Rectangular beds (cartesian):
- `MESH_MIN`
- `MESH_MAX`
- `PROBE_COUNT`
- Round beds (delta):
- `MESH_RADIUS`
- `MESH_ORIGIN`
- `ROUND_PROBE_COUNT`
- All beds:
- `RELATIVE_REFERNCE_INDEX`
- `ALGORITHM`
See the configuration documentation above for details on how each parameter
applies to the mesh.
### Profiles
`BED_MESH_PROFILE SAVE=name LOAD=name REMOVE=name`
@ -357,3 +374,9 @@ set, the generated probed points will be output to the terminal:
The "Tool Adjusted" points refer to the nozzle location for each point, and
the "Probe" points refer to the probe location. Note that when manually
probing the "Probe" points will refer to both the tool and nozzle locations.
### Clear Mesh State
`BED_MESH_CLEAR`
This gcode may be used to clear the internal mesh state.

View File

@ -192,8 +192,12 @@ The following are common printer attributes:
- `printer.query_endstops.last_query["<endstop>"]`: Returns True if
the given endstop was reported as "triggered" during the last
QUERY_ENDSTOP command. Note, due to the order of template expansion
(see above), the QUERY_STATUS command must be run prior to the macro
containing this reference.
(see above), the QUERY_ENDSTOP command must be run prior to the
macro containing this reference.
- `printer.probe.last_query`: Returns True if the probe was reported
as "triggered" during the last QUERY_PROBE command. Note, due to the
order of template expansion (see above), the QUERY_PROBE command
must be run prior to the macro containing this reference.
- `printer.configfile.config["<section>"]["<option>"]`: Returns the
given config file setting as read by Klipper during the last
software start or restart. (Any settings changed at run-time will

View File

@ -6,6 +6,10 @@ All dates in this document are approximate.
# Changes
20200902: The RTD resistance-to-temperature calculation for MAX31865
converters has been corrected to not read low. If you are using such a
device, you should recalibrate your print temperature and PID settings.
20200816: The gcode macro `printer.gcode` object has been renamed to
`printer.gcode_move`. Several undocumented variables in
`printer.toolhead` and `printer.gcode` have been removed. See

View File

@ -163,6 +163,18 @@ machine. See the
[install-octopi.sh](https://github.com/KevinOConnor/klipper/tree/master/scripts/install-octopi.sh)
script for further information on the necessary Linux admin steps.
If you are looking to run the Klipper host software on a low-end chip,
then be aware that, at a minimum, a machine with "double precision
floating point" hardware is required.
If you are looking to run the Klipper host software on a shared
general-purpose desktop or server class machine, then note that
Klipper has some real-time scheduling requirements. If, during a
print, the host computer also performs an intensive general-purpose
computing task (such as defragmenting a hard drive, 3d rendering,
heavy swapping, etc.), then it may cause Klipper to report print
errors.
Note: If you are not using an OctoPi image, be aware that several
Linux distributions enable a "ModemManager" (or similar) package that
can disrupt serial communication. (Which can cause Klipper to report

View File

@ -382,7 +382,8 @@ section is enabled:
The following commands are available when the "bed_mesh" config
section is enabled:
- `BED_MESH_CALIBRATE [METHOD=manual] [<probe_parameter>=<value>]`:
- `BED_MESH_CALIBRATE [METHOD=manual] [<probe_parameter>=<value>]
[<mesh_parameter>=<value>]`:
This command probes the bed using generated points specified by the
parameters in the config. After probing, a mesh is generated and
z-movement is adjusted according to the mesh. See the PROBE command
@ -488,13 +489,15 @@ section is enabled:
the given distance (in mm) at the given constant velocity (in
mm/s). If ACCEL is specified and is greater than zero, then the
given acceleration (in mm/s^2) will be used; otherwise no
acceleration is performed. No boundary checks are performed; no
kinematic updates are made; other parallel steppers on an axis will
not be moved. Use caution as an incorrect command could cause
damage! Using this command will almost certainly place the low-level
kinematics in an incorrect state; issue a G28 afterwards to reset
the kinematics. This command is intended for low-level diagnostics
and debugging.
acceleration is performed. If acceleration is not performed then it
can lead to the micro-controller reporting "No next step" errors
(avoid these errors by specifying an ACCEL value or use a very low
VELOCITY). No boundary checks are performed; no kinematic updates
are made; other parallel steppers on an axis will not be moved. Use
caution as an incorrect command could cause damage! Using this
command will almost certainly place the low-level kinematics in an
incorrect state; issue a G28 afterwards to reset the kinematics.
This command is intended for low-level diagnostics and debugging.
- `SET_KINEMATIC_POSITION [X=<value>] [Y=<value>] [Z=<value>]`: Force
the low-level kinematic code to believe the toolhead is at the given
cartesian position. This is a diagnostic and debugging command; use
@ -633,3 +636,17 @@ section is enabled:
[target=<target_temperature>]`: Sets the target temperature for a
temperature_fan. If a target is not supplied, it is set to the
specified temperature in the config file.
## Adxl345 Accelerometer Commands
The following command is available when an "adxl345" config section is
enabled:
- `ACCELEROMETER_MEASURE [CHIP=<config_name>] [RATE=<value>]
[NAME=<value>]`: Starts accelerometer measurements at the requested
number of samples per second. If CHIP is not specified it defaults
to "default". Valid rates are 25, 50, 100, 200, 400, 800, 1600,
and 3200. If RATE is zero (or not specified) then the current series
of measurements are stopped and the results are written to a file
named `/tmp/adxl345-<name>.csv` where "<name>" is the optional NAME
parameter. If NAME is not specified it defaults to the current time
in "YYYYMMDD_HHMMSS" format.

View File

@ -13,11 +13,11 @@ Prepping an OS image
====================
Start by installing [OctoPi](https://github.com/guysoft/OctoPi) on the
Raspberry Pi computer. Use OctoPi v0.16.0 or later - see the
Raspberry Pi computer. Use OctoPi v0.17.0 or later - see the
[octopi releases](https://github.com/guysoft/OctoPi/releases) for
release information. One should verify that OctoPi boots and that the
OctoPrint web server works. After connecting to the OctoPrint web
page, follow the prompt to upgrade OctoPrint to v1.3.12 or later.
page, follow the prompt to upgrade OctoPrint to v1.4.2 or later.
After installing OctoPi and upgrading OctoPrint, it will be necessary
to ssh into the target machine to run a handful of system commands. If

View File

@ -1,6 +1,6 @@
# Wrapper around C helper code
#
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import os, logging
@ -11,9 +11,11 @@ import cffi
# c_helper.so compiling
######################################################################
COMPILE_CMD = ("gcc -Wall -g -O2 -shared -fPIC"
" -flto -fwhole-program -fno-use-linker-plugin"
" -o %s %s")
GCC_CMD = "gcc"
COMPILE_ARGS = ("-Wall -g -O2 -shared -fPIC"
" -flto -fwhole-program -fno-use-linker-plugin"
" -o %s %s")
SSE_FLAGS = "-mfpmath=sse -msse2"
SOURCE_FILES = [
'pyhelper.c', 'serialqueue.c', 'stepcompress.c', 'itersolve.c', 'trapq.c',
'kin_cartesian.c', 'kin_corexy.c', 'kin_corexz.c', 'kin_delta.c',
@ -190,7 +192,7 @@ def get_mtimes(srcdir, filelist):
# Check if the code needs to be compiled
def check_build_code(srcdir, target, sources, cmd, other_files=[]):
src_times = get_mtimes(srcdir, sources + other_files)
src_times = get_mtimes(srcdir, sources + other_files + [__file__])
obj_times = get_mtimes(srcdir, [target])
if not obj_times or max(src_times) > min(obj_times):
logging.info("Building C code module %s", target)
@ -202,6 +204,12 @@ def check_build_code(srcdir, target, sources, cmd, other_files=[]):
logging.error(msg)
raise Exception(msg)
def check_gcc_option(option):
cmd = "%s %s -S -o /dev/null -xc /dev/null > /dev/null 2>&1" % (
GCC_CMD, option)
res = os.system(cmd)
return res == 0
FFI_main = None
FFI_lib = None
pyhelper_logging_callback = None
@ -211,8 +219,11 @@ def get_ffi():
global FFI_main, FFI_lib, pyhelper_logging_callback
if FFI_lib is None:
srcdir = os.path.dirname(os.path.realpath(__file__))
check_build_code(srcdir, DEST_LIB, SOURCE_FILES, COMPILE_CMD
, OTHER_FILES)
if check_gcc_option(SSE_FLAGS):
cmd = "%s %s %s" % (GCC_CMD, SSE_FLAGS, COMPILE_ARGS)
else:
cmd = "%s %s" % (GCC_CMD, COMPILE_ARGS)
check_build_code(srcdir, DEST_LIB, SOURCE_FILES, cmd, OTHER_FILES)
FFI_main = cffi.FFI()
for d in defs_all:
FFI_main.cdef(d)

View File

@ -13,6 +13,225 @@
#include "itersolve.h" // struct stepper_kinematics
#include "trapq.h" // struct move
/****************************************************************
* Shaper-specific initialization
****************************************************************/
struct shaper_pulses {
int num_pulses;
struct {
double t, a;
} pulses[5];
};
static inline double
calc_ZV_K(double damping_ratio)
{
if (likely(!damping_ratio))
return 1.;
return exp(-damping_ratio * M_PI / sqrt(1. - damping_ratio*damping_ratio));
}
static inline double
calc_half_period(double shaper_freq, double damping_ratio)
{
return .5 / (shaper_freq * sqrt(1. - damping_ratio*damping_ratio));
}
static void
init_shaper_zv(double shaper_freq, double damping_ratio
, struct shaper_pulses *sp)
{
sp->num_pulses = 2;
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double inv_D = 1. / (1. + K);
sp->pulses[0].t = -half_period;
sp->pulses[1].t = 0.;
sp->pulses[0].a = K * inv_D;
sp->pulses[1].a = inv_D;
}
static void
init_shaper_zvd(double shaper_freq, double damping_ratio
, struct shaper_pulses *sp)
{
sp->num_pulses = 3;
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double K2 = K * K;
double inv_D = 1. / (K2 + 2. * K + 1.);
sp->pulses[0].t = -2. * half_period;
sp->pulses[1].t = -half_period;
sp->pulses[2].t = 0.;
sp->pulses[0].a = K2 * inv_D;
sp->pulses[1].a = 2. * K * inv_D;
sp->pulses[2].a = inv_D;
}
static void
init_shaper_mzv(double shaper_freq, double damping_ratio
, struct shaper_pulses *sp)
{
sp->num_pulses = 3;
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = exp(-.75 * damping_ratio * M_PI
/ sqrt(1. - damping_ratio*damping_ratio));
double a1 = 1. - 1. / sqrt(2.);
double a2 = (sqrt(2.) - 1.) * K;
double a3 = a1 * K * K;
double inv_D = 1. / (a1 + a2 + a3);
sp->pulses[0].t = -1.5 * half_period;
sp->pulses[1].t = -.75 * half_period;
sp->pulses[2].t = 0.;
sp->pulses[0].a = a3 * inv_D;
sp->pulses[1].a = a2 * inv_D;
sp->pulses[2].a = a1 * inv_D;
}
#define EI_SHAPER_VIB_TOL 0.05
static void
init_shaper_ei(double shaper_freq, double damping_ratio
, struct shaper_pulses *sp)
{
sp->num_pulses = 3;
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double a1 = .25 * (1. + EI_SHAPER_VIB_TOL);
double a2 = .5 * (1. - EI_SHAPER_VIB_TOL) * K;
double a3 = a1 * K * K;
double inv_D = 1. / (a1 + a2 + a3);
sp->pulses[0].t = -2. * half_period;
sp->pulses[1].t = -half_period;
sp->pulses[2].t = 0.;
sp->pulses[0].a = a3 * inv_D;
sp->pulses[1].a = a2 * inv_D;
sp->pulses[2].a = a1 * inv_D;
}
static void
init_shaper_2hump_ei(double shaper_freq, double damping_ratio
, struct shaper_pulses *sp)
{
sp->num_pulses = 4;
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double V2 = EI_SHAPER_VIB_TOL * EI_SHAPER_VIB_TOL;
double X = pow(V2 * (sqrt(1. - V2) + 1.), 1./3.);
double a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X);
double a2 = (.5 - a1) * K;
double a3 = a2 * K;
double a4 = a1 * K * K * K;
double inv_D = 1. / (a1 + a2 + a3 + a4);
sp->pulses[0].t = -3. * half_period;
sp->pulses[1].t = -2. * half_period;
sp->pulses[2].t = -half_period;
sp->pulses[3].t = 0.;
sp->pulses[0].a = a4 * inv_D;
sp->pulses[1].a = a3 * inv_D;
sp->pulses[2].a = a2 * inv_D;
sp->pulses[3].a = a1 * inv_D;
}
static void
init_shaper_3hump_ei(double shaper_freq, double damping_ratio
, struct shaper_pulses *sp)
{
sp->num_pulses = 5;
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double K2 = K * K;
double a1 = 0.0625 * (1. + 3. * EI_SHAPER_VIB_TOL
+ 2. * sqrt(2. * (EI_SHAPER_VIB_TOL + 1.) * EI_SHAPER_VIB_TOL));
double a2 = 0.25 * (1. - EI_SHAPER_VIB_TOL) * K;
double a3 = (0.5 * (1. + EI_SHAPER_VIB_TOL) - 2. * a1) * K2;
double a4 = a2 * K2;
double a5 = a1 * K2 * K2;
double inv_D = 1. / (a1 + a2 + a3 + a4 + a5);
sp->pulses[0].t = -4. * half_period;
sp->pulses[1].t = -3. * half_period;
sp->pulses[2].t = -2. * half_period;
sp->pulses[3].t = -half_period;
sp->pulses[4].t = 0.;
sp->pulses[0].a = a5 * inv_D;
sp->pulses[1].a = a4 * inv_D;
sp->pulses[2].a = a3 * inv_D;
sp->pulses[3].a = a2 * inv_D;
sp->pulses[4].a = a1 * inv_D;
}
// Shift pulses around 'mid-point' t=0 so that the input shaper is an identity
// transformation for constant-speed motion (i.e. input_shaper(v * T) = v * T)
static void
shift_pulses(struct shaper_pulses *sp)
{
int i;
double ts = 0.;
for (i = 0; i < sp->num_pulses; ++i)
ts += sp->pulses[i].a * sp->pulses[i].t;
for (i = 0; i < sp->num_pulses; ++i)
sp->pulses[i].t -= ts;
}
enum INPUT_SHAPER_TYPE {
INPUT_SHAPER_ZV = 0,
INPUT_SHAPER_ZVD = 1,
INPUT_SHAPER_MZV = 2,
INPUT_SHAPER_EI = 3,
INPUT_SHAPER_2HUMP_EI = 4,
INPUT_SHAPER_3HUMP_EI = 5,
};
typedef void (*is_init_shaper_callback)(double shaper_freq
, double damping_ratio
, struct shaper_pulses *sp);
static is_init_shaper_callback init_shaper_callbacks[] = {
[INPUT_SHAPER_ZV] = &init_shaper_zv,
[INPUT_SHAPER_ZVD] = &init_shaper_zvd,
[INPUT_SHAPER_MZV] = &init_shaper_mzv,
[INPUT_SHAPER_EI] = &init_shaper_ei,
[INPUT_SHAPER_2HUMP_EI] = &init_shaper_2hump_ei,
[INPUT_SHAPER_3HUMP_EI] = &init_shaper_3hump_ei,
};
static void
init_shaper(int shaper_type, double shaper_freq, double damping_ratio
, struct shaper_pulses *sp)
{
if (shaper_type < 0 || shaper_type >= ARRAY_SIZE(init_shaper_callbacks)
|| shaper_freq <= 0.) {
sp->num_pulses = 0;
return;
}
init_shaper_callbacks[shaper_type](shaper_freq, damping_ratio, sp);
shift_pulses(sp);
}
/****************************************************************
* Generic position calculation via shaper convolution
****************************************************************/
@ -40,224 +259,20 @@ get_axis_position_across_moves(struct move *m, int axis, double time)
return get_axis_position(m, axis, time);
}
struct shaper_pulse {
double t, a;
};
// Calculate the position from the convolution of the shaper with input signal
static inline double
calc_position(struct move *m, int axis, double move_time
, struct shaper_pulse *pulses, int n)
, struct shaper_pulses *sp)
{
double res = 0.;
int i;
for (i = 0; i < n; ++i)
res += pulses[i].a * get_axis_position_across_moves(
m, axis, move_time + pulses[i].t);
int num_pulses = sp->num_pulses, i;
for (i = 0; i < num_pulses; ++i) {
double t = sp->pulses[i].t, a = sp->pulses[i].a;
res += a * get_axis_position_across_moves(m, axis, move_time + t);
}
return res;
}
/****************************************************************
* Shaper-specific initialization
****************************************************************/
#define EI_SHAPER_VIB_TOL 0.05
enum INPUT_SHAPER_TYPE {
INPUT_SHAPER_ZV = 0,
INPUT_SHAPER_ZVD = 1,
INPUT_SHAPER_MZV = 2,
INPUT_SHAPER_EI = 3,
INPUT_SHAPER_2HUMP_EI = 4,
INPUT_SHAPER_3HUMP_EI = 5,
};
struct input_shaper {
struct stepper_kinematics sk;
struct stepper_kinematics *orig_sk;
struct move m;
struct shaper_pulse *x_pulses, *y_pulses;
int x_n, y_n;
};
typedef void (*is_init_shaper_callback)(double shaper_freq
, double damping_ratio
, struct shaper_pulse **pulses, int *n);
static inline double
calc_ZV_K(double damping_ratio)
{
if (likely(!damping_ratio))
return 1.;
return exp(-damping_ratio * M_PI / sqrt(1. - damping_ratio*damping_ratio));
}
static inline double
calc_half_period(double shaper_freq, double damping_ratio)
{
return .5 / (shaper_freq * sqrt(1. - damping_ratio*damping_ratio));
}
static void
init_shaper_zv(double shaper_freq, double damping_ratio
, struct shaper_pulse **pulses, int *n)
{
*n = 2;
*pulses = malloc(*n * sizeof(struct shaper_pulse));
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double inv_D = 1. / (1. + K);
(*pulses)[0].t = -half_period;
(*pulses)[1].t = 0.;
(*pulses)[0].a = K * inv_D;
(*pulses)[1].a = inv_D;
}
static void
init_shaper_zvd(double shaper_freq, double damping_ratio
, struct shaper_pulse **pulses, int *n)
{
*n = 3;
*pulses = malloc(*n * sizeof(struct shaper_pulse));
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double K2 = K * K;
double inv_D = 1. / (K2 + 2. * K + 1.);
(*pulses)[0].t = -2. * half_period;
(*pulses)[1].t = -half_period;
(*pulses)[2].t = 0.;
(*pulses)[0].a = K2 * inv_D;
(*pulses)[1].a = 2. * K * inv_D;
(*pulses)[2].a = inv_D;
}
static void
init_shaper_mzv(double shaper_freq, double damping_ratio
, struct shaper_pulse **pulses, int *n)
{
*n = 3;
*pulses = malloc(*n * sizeof(struct shaper_pulse));
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = exp(-.75 * damping_ratio * M_PI
/ sqrt(1. - damping_ratio*damping_ratio));
double a1 = 1. - 1. / sqrt(2.);
double a2 = (sqrt(2.) - 1.) * K;
double a3 = a1 * K * K;
double inv_D = 1. / (a1 + a2 + a3);
(*pulses)[0].t = -1.5 * half_period;
(*pulses)[1].t = -.75 * half_period;
(*pulses)[2].t = 0.;
(*pulses)[0].a = a3 * inv_D;
(*pulses)[1].a = a2 * inv_D;
(*pulses)[2].a = a1 * inv_D;
}
static void
init_shaper_ei(double shaper_freq, double damping_ratio
, struct shaper_pulse **pulses, int *n)
{
*n = 3;
*pulses = malloc(*n * sizeof(struct shaper_pulse));
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double a1 = .25 * (1. + EI_SHAPER_VIB_TOL);
double a2 = .5 * (1. - EI_SHAPER_VIB_TOL) * K;
double a3 = a1 * K * K;
double inv_D = 1. / (a1 + a2 + a3);
(*pulses)[0].t = -2. * half_period;
(*pulses)[1].t = -half_period;
(*pulses)[2].t = 0.;
(*pulses)[0].a = a3 * inv_D;
(*pulses)[1].a = a2 * inv_D;
(*pulses)[2].a = a1 * inv_D;
}
static void
init_shaper_2hump_ei(double shaper_freq, double damping_ratio
, struct shaper_pulse **pulses, int *n)
{
*n = 4;
*pulses = malloc(*n * sizeof(struct shaper_pulse));
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double V2 = EI_SHAPER_VIB_TOL * EI_SHAPER_VIB_TOL;
double X = pow(V2 * (sqrt(1. - V2) + 1.), 1./3.);
double a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X);
double a2 = (.5 - a1) * K;
double a3 = a2 * K;
double a4 = a1 * K * K * K;
double inv_D = 1. / (a1 + a2 + a3 + a4);
(*pulses)[0].t = -3. * half_period;
(*pulses)[1].t = -2. * half_period;
(*pulses)[2].t = -half_period;
(*pulses)[3].t = 0.;
(*pulses)[0].a = a4 * inv_D;
(*pulses)[1].a = a3 * inv_D;
(*pulses)[2].a = a2 * inv_D;
(*pulses)[3].a = a1 * inv_D;
}
static void
init_shaper_3hump_ei(double shaper_freq, double damping_ratio
, struct shaper_pulse **pulses, int *n)
{
*n = 5;
*pulses = malloc(*n * sizeof(struct shaper_pulse));
double half_period = calc_half_period(shaper_freq, damping_ratio);
double K = calc_ZV_K(damping_ratio);
double K2 = K * K;
double a1 = 0.0625 * (1. + 3. * EI_SHAPER_VIB_TOL
+ 2. * sqrt(2. * (EI_SHAPER_VIB_TOL + 1.) * EI_SHAPER_VIB_TOL));
double a2 = 0.25 * (1. - EI_SHAPER_VIB_TOL) * K;
double a3 = (0.5 * (1. + EI_SHAPER_VIB_TOL) - 2. * a1) * K2;
double a4 = a2 * K2;
double a5 = a1 * K2 * K2;
double inv_D = 1. / (a1 + a2 + a3 + a4 + a5);
(*pulses)[0].t = -4. * half_period;
(*pulses)[1].t = -3. * half_period;
(*pulses)[2].t = -2. * half_period;
(*pulses)[3].t = -half_period;
(*pulses)[4].t = 0.;
(*pulses)[0].a = a5 * inv_D;
(*pulses)[1].a = a4 * inv_D;
(*pulses)[2].a = a3 * inv_D;
(*pulses)[3].a = a2 * inv_D;
(*pulses)[4].a = a1 * inv_D;
}
// Shift pulses around 'mid-point' t=0 so that the input shaper is an identity
// transformation for constant-speed motion (i.e. input_shaper(v * T) = v * T)
static void
shift_pulses(int n, struct shaper_pulse *pulses)
{
int i;
double ts = 0.;
for (i = 0; i < n; ++i)
ts += pulses[i].a * pulses[i].t;
for (i = 0; i < n; ++i)
pulses[i].t -= ts;
}
/****************************************************************
* Kinematics-related shaper code
@ -265,15 +280,22 @@ shift_pulses(int n, struct shaper_pulse *pulses)
#define DUMMY_T 500.0
struct input_shaper {
struct stepper_kinematics sk;
struct stepper_kinematics *orig_sk;
struct move m;
struct shaper_pulses sx, sy;
};
// Optimized calc_position when only x axis is needed
static double
shaper_x_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->x_n)
if (!is->sx.num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos.x = calc_position(m, 'x', move_time, is->x_pulses, is->x_n);
is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
@ -283,9 +305,9 @@ shaper_y_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->y_n)
if (!is->sy.num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos.y = calc_position(m, 'y', move_time, is->y_pulses, is->y_n);
is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
@ -295,48 +317,27 @@ shaper_xy_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (!is->x_n && !is->y_n)
if (!is->sx.num_pulses && !is->sy.num_pulses)
return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos = move_get_coord(m, move_time);
if (is->x_n)
is->m.start_pos.x = calc_position(m, 'x', move_time
, is->x_pulses, is->x_n);
if (is->y_n)
is->m.start_pos.y = calc_position(m, 'y', move_time
, is->y_pulses, is->y_n);
if (is->sx.num_pulses)
is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx);
if (is->sy.num_pulses)
is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T);
}
static void
shaper_note_generation_time(struct input_shaper *is)
{
double pre_active = 0., post_active = 0.;
if ((is->sk.active_flags & AF_X) && is->x_n) {
pre_active = is->x_pulses[is->x_n-1].t;
post_active = -is->x_pulses[0].t;
}
if ((is->sk.active_flags & AF_Y) && is->y_n) {
pre_active = is->y_pulses[is->y_n-1].t > pre_active
? is->y_pulses[is->y_n-1].t : pre_active;
post_active = -is->y_pulses[0].t > post_active
? -is->y_pulses[0].t : post_active;
}
is->sk.gen_steps_pre_active = pre_active;
is->sk.gen_steps_post_active = post_active;
}
int __visible
input_shaper_set_sk(struct stepper_kinematics *sk
, struct stepper_kinematics *orig_sk)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
int af = orig_sk->active_flags & (AF_X | AF_Y);
if (af == (AF_X | AF_Y))
is->sk.calc_position_cb = shaper_xy_calc_position;
else if (af & AF_X)
if (orig_sk->active_flags == AF_X)
is->sk.calc_position_cb = shaper_x_calc_position;
else if (af & AF_Y)
else if (orig_sk->active_flags == AF_Y)
is->sk.calc_position_cb = shaper_y_calc_position;
else if (orig_sk->active_flags & (AF_X | AF_Y))
is->sk.calc_position_cb = shaper_xy_calc_position;
else
return -1;
is->sk.active_flags = orig_sk->active_flags;
@ -344,14 +345,23 @@ input_shaper_set_sk(struct stepper_kinematics *sk
return 0;
}
static is_init_shaper_callback init_shaper_callbacks[] = {
[INPUT_SHAPER_ZV] = &init_shaper_zv,
[INPUT_SHAPER_ZVD] = &init_shaper_zvd,
[INPUT_SHAPER_MZV] = &init_shaper_mzv,
[INPUT_SHAPER_EI] = &init_shaper_ei,
[INPUT_SHAPER_2HUMP_EI] = &init_shaper_2hump_ei,
[INPUT_SHAPER_3HUMP_EI] = &init_shaper_3hump_ei,
};
static void
shaper_note_generation_time(struct input_shaper *is)
{
double pre_active = 0., post_active = 0.;
if ((is->sk.active_flags & AF_X) && is->sx.num_pulses) {
pre_active = is->sx.pulses[is->sx.num_pulses-1].t;
post_active = -is->sx.pulses[0].t;
}
if ((is->sk.active_flags & AF_Y) && is->sy.num_pulses) {
pre_active = is->sy.pulses[is->sy.num_pulses-1].t > pre_active
? is->sy.pulses[is->sy.num_pulses-1].t : pre_active;
post_active = -is->sy.pulses[0].t > post_active
? -is->sy.pulses[0].t : post_active;
}
is->sk.gen_steps_pre_active = pre_active;
is->sk.gen_steps_post_active = post_active;
}
int __visible
input_shaper_set_shaper_params(struct stepper_kinematics *sk
@ -363,31 +373,14 @@ input_shaper_set_shaper_params(struct stepper_kinematics *sk
, double damping_ratio_y)
{
struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (shaper_type_x >= ARRAY_SIZE(init_shaper_callbacks) || shaper_type_x < 0)
return -1;
if (shaper_type_y >= ARRAY_SIZE(init_shaper_callbacks) || shaper_type_y < 0)
return -1;
int af = is->orig_sk->active_flags & (AF_X | AF_Y);
free(is->x_pulses);
if ((af & AF_X) && shaper_freq_x > 0.) {
init_shaper_callbacks[shaper_type_x](
shaper_freq_x, damping_ratio_x, &is->x_pulses, &is->x_n);
shift_pulses(is->x_n, is->x_pulses);
} else {
is->x_pulses = NULL;
is->x_n = 0;
}
free(is->y_pulses);
if ((af & AF_Y) && shaper_freq_y > 0.) {
init_shaper_callbacks[shaper_type_y](
shaper_freq_y, damping_ratio_y, &is->y_pulses, &is->y_n);
shift_pulses(is->y_n, is->y_pulses);
} else {
is->y_pulses = NULL;
is->y_n = 0;
}
if (is->orig_sk->active_flags & AF_X)
init_shaper(shaper_type_x, shaper_freq_x, damping_ratio_x, &is->sx);
else
is->sx.num_pulses = 0;
if (is->orig_sk->active_flags & AF_Y)
init_shaper(shaper_type_y, shaper_freq_y, damping_ratio_y, &is->sy);
else
is->sy.num_pulses = 0;
shaper_note_generation_time(is);
return 0;
}
@ -396,19 +389,13 @@ double __visible
input_shaper_get_step_generation_window(int shaper_type, double shaper_freq
, double damping_ratio)
{
if (shaper_freq <= 0.)
struct shaper_pulses sp;
init_shaper(shaper_type, shaper_freq, damping_ratio, &sp);
if (!sp.num_pulses)
return 0.;
if (shaper_type >= ARRAY_SIZE(init_shaper_callbacks) || shaper_type < 0)
return 0.;
is_init_shaper_callback init_shaper_cb = init_shaper_callbacks[shaper_type];
int n;
struct shaper_pulse *pulses;
init_shaper_cb(shaper_freq, damping_ratio, &pulses, &n);
shift_pulses(n, pulses);
double window = -pulses[0].t;
if (pulses[n-1].t > window)
window = pulses[n-1].t;
free(pulses);
double window = -sp.pulses[0].t;
if (sp.pulses[sp.num_pulses-1].t > window)
window = sp.pulses[sp.num_pulses-1].t;
return window;
}

View File

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

View File

@ -262,7 +262,7 @@ AD8497 = [
]
def calc_pt100(base=100.):
# Calc PT100/PT1000 temperature/resistance pairs using formula
# Calc PT100/PT1000 resistances using Callendar-Van Dusen formula
A, B = (3.9083e-3, -5.775e-7)
return [(float(t), base * (1. + A*t + B*t*t)) for t in range(0, 500, 10)]

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

@ -0,0 +1,225 @@
# Support for reading acceleration data from an adxl345 chip
#
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, time, collections
from . import bus
# ADXL345 registers
REG_DEVID = 0x00
REG_BW_RATE = 0x2C
REG_POWER_CTL = 0x2D
REG_DATA_FORMAT = 0x31
REG_FIFO_CTL = 0x38
REG_MOD_READ = 0x80
REG_MOD_MULTI = 0x40
QUERY_RATES = {
25: 0x8, 50: 0x9, 100: 0xa, 200: 0xb, 400: 0xc,
800: 0xd, 1600: 0xe, 3200: 0xf,
}
SCALE = 0.004 * 9.80665 * 1000. # 4mg/LSB * Earth gravity in mm/s**2
Accel_Measurement = collections.namedtuple(
'Accel_Measurement', ('time', 'accel_x', 'accel_y', 'accel_z'))
# Sample results
class ADXL345Results:
def __init__(self):
self.samples = []
self.drops = self.overflows = 0
self.time_per_sample = self.start_range = self.end_range = 0.
def get_samples(self):
return self.samples
def get_stats(self):
return ("drops=%d,overflows=%d"
",time_per_sample=%.9f,start_range=%.6f,end_range=%.6f"
% (self.drops, self.overflows,
self.time_per_sample, self.start_range, self.end_range))
def setup_data(self, axes_map, raw_samples, end_sequence, overflows,
start1_time, start2_time, end1_time, end2_time):
if not raw_samples or not end_sequence:
return
(x_pos, x_scale), (y_pos, y_scale), (z_pos, z_scale) = axes_map
self.overflows = overflows
self.start_range = start2_time - start1_time
self.end_range = end2_time - end1_time
total_count = (end_sequence - 1) * 8 + len(raw_samples[-1][1]) // 6
total_time = end2_time - start2_time
self.time_per_sample = time_per_sample = total_time / total_count
seq_to_time = time_per_sample * 8.
self.samples = samples = [None] * total_count
actual_count = 0
for seq, data in raw_samples:
d = bytearray(data)
count = len(data)
sdata = [(d[i] | (d[i+1] << 8)) - ((d[i+1] & 0x80) << 9)
for i in range(0, count-1, 2)]
seq_time = start2_time + seq * seq_to_time
for i in range(count//6):
samp_time = seq_time + i * time_per_sample
x = sdata[i*3 + x_pos] * x_scale
y = sdata[i*3 + y_pos] * y_scale
z = sdata[i*3 + z_pos] * z_scale
samples[actual_count] = Accel_Measurement(samp_time, x, y, z)
actual_count += 1
del samples[actual_count:]
self.drops = total_count - actual_count
# Printer class that controls measurments
class ADXL345:
def __init__(self, config):
self.printer = config.get_printer()
self.query_rate = 0
self.last_tx_time = 0.
am = {'x': (0, SCALE), 'y': (1, SCALE), 'z': (2, SCALE),
'-x': (0, -SCALE), '-y': (1, -SCALE), '-z': (2, -SCALE)}
axes_map = config.get('axes_map', 'x,y,z').split(',')
if len(axes_map) != 3 or any([a.strip() 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]
# Measurement storage (accessed from background thread)
self.raw_samples = []
self.last_sequence = 0
self.samples_start1 = self.samples_start2 = 0.
# Setup mcu sensor_adxl345 bulk query code
self.spi = bus.MCU_SPI_from_config(config, 3, default_speed=5000000)
self.mcu = mcu = self.spi.get_mcu()
self.oid = oid = mcu.create_oid()
self.query_adxl345_cmd = self.query_adxl345_end_cmd =None
mcu.add_config_cmd("config_adxl345 oid=%d spi_oid=%d"
% (oid, self.spi.get_oid()))
mcu.add_config_cmd("query_adxl345 oid=%d clock=0 rest_ticks=0"
% (oid,), on_restart=True)
mcu.register_config_callback(self._build_config)
mcu.register_response(self._handle_adxl345_start, "adxl345_start", oid)
mcu.register_response(self._handle_adxl345_data, "adxl345_data", oid)
# Register commands
name = "default"
if len(config.get_name().split()) > 1:
name = config.get_name().split()[1]
gcode = self.printer.lookup_object('gcode')
gcode.register_mux_command("ACCELEROMETER_MEASURE", "CHIP", name,
self.cmd_ACCELEROMETER_MEASURE,
desc=self.cmd_ACCELEROMETER_MEASURE_help)
if name == "default":
gcode.register_mux_command("ACCELEROMETER_MEASURE", "CHIP", None,
self.cmd_ACCELEROMETER_MEASURE)
def _build_config(self):
self.query_adxl345_cmd = self.mcu.lookup_command(
"query_adxl345 oid=%c clock=%u rest_ticks=%u",
cq=self.spi.get_command_queue())
self.query_adxl345_end_cmd = self.mcu.lookup_query_command(
"query_adxl345 oid=%c clock=%u rest_ticks=%u",
"adxl345_end oid=%c end1_time=%u end2_time=%u"
" limit_count=%hu sequence=%hu",
oid=self.oid, cq=self.spi.get_command_queue())
def _clock_to_print_time(self, clock):
return self.mcu.clock_to_print_time(self.mcu.clock32_to_clock64(clock))
def _handle_adxl345_start(self, params):
self.samples_start1 = self._clock_to_print_time(params['start1_time'])
self.samples_start2 = self._clock_to_print_time(params['start2_time'])
def _handle_adxl345_data(self, params):
last_sequence = self.last_sequence
sequence = (last_sequence & ~0xffff) | params['sequence']
if sequence < last_sequence:
sequence += 0x10000
self.last_sequence = sequence
raw_samples = self.raw_samples
if len(raw_samples) >= 200000:
# Avoid filling up memory with too many samples
return
raw_samples.append((sequence, params['data']))
def _convert_sequence(self, sequence):
sequence = (self.last_sequence & ~0xffff) | sequence
if sequence < self.last_sequence:
sequence += 0x10000
return sequence
def start_measurements(self, rate=None):
if rate is None:
rate = 3200
# Verify chip connectivity
params = self.spi.spi_transfer([REG_DEVID | REG_MOD_READ, 0x00])
response = bytearray(params['response'])
if response[1] != 0xe5:
raise self.printer.command_error("Invalid adxl345 id (got %x vs %x)"
% (response[1], 0xe5))
# Setup chip in requested query rate
clock = 0
if self.last_tx_time:
clock = self.mcu.print_time_to_clock(self.last_tx_time)
self.spi.spi_send([REG_POWER_CTL, 0x00], minclock=clock)
self.spi.spi_send([REG_FIFO_CTL, 0x00])
self.spi.spi_send([REG_DATA_FORMAT, 0x0B])
self.spi.spi_send([REG_BW_RATE, QUERY_RATES[rate]])
self.spi.spi_send([REG_FIFO_CTL, 0x80])
# Setup samples
print_time = self.printer.lookup_object('toolhead').get_last_move_time()
self.raw_samples = []
self.last_sequence = 0
self.samples_start1 = self.samples_start2 = print_time
# Start bulk reading
reqclock = self.mcu.print_time_to_clock(print_time)
rest_ticks = self.mcu.seconds_to_clock(4. / rate)
self.last_tx_time = print_time
self.query_rate = rate
self.query_adxl345_cmd.send([self.oid, reqclock, rest_ticks],
reqclock=reqclock)
def finish_measurements(self):
query_rate = self.query_rate
if not query_rate:
return ADXL345Results()
# Halt bulk reading
print_time = self.printer.lookup_object('toolhead').get_last_move_time()
clock = self.mcu.print_time_to_clock(print_time)
params = self.query_adxl345_end_cmd.send([self.oid, 0, 0],
minclock=clock)
self.last_tx_time = print_time
self.query_rate = 0
raw_samples = self.raw_samples
self.raw_samples = []
# Generate results
end1_time = self._clock_to_print_time(params['end1_time'])
end2_time = self._clock_to_print_time(params['end2_time'])
end_sequence = self._convert_sequence(params['sequence'])
overflows = params['limit_count']
res = ADXL345Results()
res.setup_data(self.axes_map, raw_samples, end_sequence, overflows,
self.samples_start1, self.samples_start2,
end1_time, end2_time)
logging.info("ADXL345 finished %d measurements: %s",
len(res.get_samples()), res.get_stats())
return res
def end_query(self, name):
if not self.query_rate:
return
res = self.finish_measurements()
# Write data to file
f = open("/tmp/adxl345-%s.csv" % (name,), "w")
f.write("##%s\n#time,accel_x,accel_y,accel_z\n" % (res.get_stats(),))
for t, accel_x, accel_y, accel_z in res.get_samples():
f.write("%.6f,%.6f,%.6f,%.6f\n" % (t, accel_x, accel_y, accel_z))
f.close()
cmd_ACCELEROMETER_MEASURE_help = "Start/stop accelerometer"
def cmd_ACCELEROMETER_MEASURE(self, gcmd):
rate = gcmd.get_int("RATE", 0)
if not rate:
name = gcmd.get("NAME", time.strftime("%Y%m%d_%H%M%S"))
if not name.replace('-', '').replace('_', '').isalnum():
raise gcmd.error("Invalid adxl345 NAME parameter")
self.end_query(name)
gcmd.respond_info("adxl345 measurements stopped")
elif self.query_rate:
raise gcmd.error("adxl345 already running")
elif rate not in QUERY_RATES:
raise gcmd.error("Not a valid adxl345 query rate")
else:
self.start_measurements(rate)
def load_config(config):
return ADXL345(config)
def load_config_prefix(config):
return ADXL345(config)

View File

@ -235,13 +235,17 @@ class BedMeshCalibrate:
ALGOS = ['lagrange', 'bicubic']
def __init__(self, config, bedmesh):
self.printer = config.get_printer()
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.orig_config['rri'] = self.relative_reference_index
self.bedmesh = bedmesh
self.mesh_config = collections.OrderedDict()
self.points = self._generate_points(config)
self._init_mesh_config(config, self.points)
self._init_mesh_config(config)
self._generate_points(config.error)
self.orig_points = self.points
self.probe_helper = probe.ProbePointsHelper(
config, self.probe_finalize, self.points)
self.probe_helper.minimum_points(3)
@ -250,37 +254,18 @@ 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, config):
self.radius = config.getfloat('mesh_radius', None, above=0.)
if self.radius is not None:
self.origin = parse_pair(config, ('mesh_origin', "0, 0"))
x_cnt = y_cnt = config.getint('round_probe_count', 5, minval=3)
# round beds must have an odd number of points along each axis
if not x_cnt & 1:
raise config.error(
"bed_mesh: probe_count must be odd for round beds")
# radius may have precision to .1mm
self.radius = math.floor(self.radius * 10) / 10
min_x = min_y = -self.radius
max_x = max_y = self.radius
else:
# rectangular
x_cnt, y_cnt = parse_pair(
config, ('probe_count', '3'), check=False, cast=int, minval=3)
min_x, min_y = parse_pair(config, ('mesh_min',))
max_x, max_y = parse_pair(config, ('mesh_max',))
if max_x <= min_x or max_y <= min_y:
raise config.error('bed_mesh: invalid min/max points')
self.mesh_config['x_count'] = x_cnt
self.mesh_config['y_count'] = y_cnt
def _generate_points(self, error):
x_cnt = self.mesh_config['x_count']
y_cnt = self.mesh_config['y_count']
min_x, min_y = self.mesh_min
max_x, max_y = self.mesh_max
x_dist = (max_x - min_x) / (x_cnt - 1)
y_dist = (max_y - min_y) / (y_cnt - 1)
# 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.:
raise config.error("bed_mesh: min/max points too close together")
raise error("bed_mesh: min/max points too close together")
if self.radius is not None:
# round bed, min/max needs to be recalculated
@ -311,7 +296,7 @@ class BedMeshCalibrate:
points.append(
(self.origin[0] + pos_x, self.origin[1] + pos_y))
pos_y += y_dist
return points
self.points = points
def print_generated_points(self, print_func):
x_offset = y_offset = 0.
probe = self.printer.lookup_object('probe', None)
@ -329,33 +314,69 @@ class BedMeshCalibrate:
print_func(
"bed_mesh: relative_reference_index %d is (%.2f, %.2f)"
% (rri, self.points[rri][0], self.points[rri][1]))
def _init_mesh_config(self, config, points):
def _init_mesh_config(self, config):
mesh_cfg = self.mesh_config
orig_cfg = self.orig_config
self.radius = config.getfloat('mesh_radius', None, above=0.)
if self.radius is not None:
self.origin = parse_pair(config, ('mesh_origin', "0, 0"))
x_cnt = y_cnt = config.getint('round_probe_count', 5, minval=3)
# round beds must have an odd number of points along each axis
if not x_cnt & 1:
raise config.error(
"bed_mesh: probe_count must be odd for round beds")
# radius may have precision to .1mm
self.radius = math.floor(self.radius * 10) / 10
orig_cfg['radius'] = self.radius
orig_cfg['origin'] = self.origin
min_x = min_y = -self.radius
max_x = max_y = self.radius
else:
# rectangular
x_cnt, y_cnt = parse_pair(
config, ('probe_count', '3'), check=False, cast=int, minval=3)
min_x, min_y = parse_pair(config, ('mesh_min',))
max_x, max_y = parse_pair(config, ('mesh_max',))
if max_x <= min_x or max_y <= min_y:
raise config.error('bed_mesh: invalid min/max points')
orig_cfg['x_count'] = mesh_cfg['x_count'] = x_cnt
orig_cfg['y_count'] = mesh_cfg['y_count'] = y_cnt
orig_cfg['mesh_min'] = self.mesh_min = (min_x, min_y)
orig_cfg['mesh_max'] = self.mesh_max = (max_x, max_y)
pps = parse_pair(config, ('mesh_pps', '2'), check=False,
cast=int, minval=0)
orig_cfg['mesh_x_pps'] = mesh_cfg['mesh_x_pps'] = pps[0]
orig_cfg['mesh_y_pps'] = mesh_cfg['mesh_y_pps'] = pps[1]
orig_cfg['algo'] = mesh_cfg['algo'] = \
config.get('algorithm', 'lagrange').strip().lower()
orig_cfg['tension'] = mesh_cfg['tension'] = config.getfloat(
'bicubic_tension', .2, minval=0., maxval=2.)
self._verify_algorithm(config.error)
def _verify_algorithm(self, error):
params = self.mesh_config
params['mesh_x_pps'] = pps[0]
params['mesh_y_pps'] = pps[1]
params['algo'] = config.get('algorithm', 'lagrange').strip().lower()
x_pps = params['mesh_x_pps']
y_pps = params['mesh_y_pps']
if params['algo'] not in self.ALGOS:
raise config.error(
raise error(
"bed_mesh: Unknown algorithm <%s>"
% (self.mesh_config['algo']))
# Check the algorithm against the current configuration
max_probe_cnt = max(params['x_count'], params['y_count'])
min_probe_cnt = min(params['x_count'], params['y_count'])
if max(pps[0], pps[1]) == 0:
if max(x_pps, y_pps) == 0:
# Interpolation disabled
self.mesh_config['algo'] = 'direct'
elif params['algo'] == 'lagrange' and max_probe_cnt > 6:
# Lagrange interpolation tends to oscillate when using more
# than 6 samples
raise config.error(
raise error(
"bed_mesh: cannot exceed a probe_count of 6 when using "
"lagrange interpolation. Configured Probe Count: %d, %d" %
(self.mesh_config['x_count'], self.mesh_config['y_count']))
elif params['algo'] == 'bicubic' and min_probe_cnt < 4:
if max_probe_cnt > 6:
raise config.error(
raise error(
"bed_mesh: invalid probe_count option when using bicubic "
"interpolation. Combination of 3 points on one axis with "
"more than 6 on another is not permitted. "
@ -368,11 +389,74 @@ class BedMeshCalibrate:
"interpolation. Configured Probe Count: %d, %d" %
(self.mesh_config['x_count'], self.mesh_config['y_count']))
params['algo'] = 'lagrange'
params['tension'] = config.getfloat(
'bicubic_tension', .2, minval=0., maxval=2.)
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()):
self.mesh_config[key] = self.orig_config[key]
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')
need_cfg_update = True
if self.radius is not None:
if "MESH_RADIUS" in params:
self.radius = gcmd.get_float("MESH_RADIUS")
self.radius = math.floor(self.radius * 10) / 10
self.mesh_min = (-self.radius, -self.radius)
self.mesh_max = (self.radius, self.radius)
need_cfg_update = True
if "MESH_ORIGIN" in params:
self.origin = parse_pair(gcmd, ('MESH_ORIGIN',))
need_cfg_update = True
if "ROUND_PROBE_COUNT" in params:
cnt = gcmd.get_int('ROUND_PROBE_COUNT', minval=3)
self.mesh_config['x_count'] = cnt
self.mesh_config['y_count'] = cnt
need_cfg_update = True
else:
if "MESH_MIN" in params:
self.mesh_min = parse_pair(gcmd, ('MESH_MIN',))
need_cfg_update = True
if "MESH_MAX" in params:
self.mesh_max = parse_pair(gcmd, ('MESH_MAX',))
need_cfg_update = True
if "PROBE_COUNT" in params:
x_cnt, y_cnt = parse_pair(
gcmd, ('PROBE_COUNT',), check=False, cast=int, minval=3)
self.mesh_config['x_count'] = x_cnt
self.mesh_config['y_count'] = y_cnt
need_cfg_update = True
if "ALGORITHM" in params:
self.mesh_config['algo'] = gcmd.get('ALGORITHM').strip().lower()
need_cfg_update = True
if need_cfg_update:
self._verify_algorithm(gcmd.error)
self._generate_points(gcmd.error)
gcmd.respond_info("Generating new points...")
self.print_generated_points(gcmd.respond_info)
self.probe_helper.update_probe_points(self.points, 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()])
logging.info("Updated Mesh Configuration:\n" + msg)
else:
self.points = self.orig_points
self.probe_helper.update_probe_points(self.points, 3)
cmd_BED_MESH_CALIBRATE_help = "Perform Mesh Bed Leveling"
def cmd_BED_MESH_CALIBRATE(self, gcmd):
self.bedmesh.set_mesh(None)
self.update_config(gcmd)
self.probe_helper.start_probe(gcmd)
def probe_finalize(self, offsets, positions):
x_offset, y_offset, z_offset = offsets

View File

@ -3,7 +3,6 @@
# Copyright (C) 2019 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import homing
def parse_coord(config, param):
pair = config.get(param).strip().split(',', 1)

View File

@ -4,7 +4,6 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import homing
from . import probe
SIGNAL_PERIOD = 0.020
@ -82,7 +81,7 @@ class BLTouchEndstopWrapper:
self.set_output_mode(self.output_mode)
try:
self.raise_probe()
except homing.CommandError as e:
except self.printer.command_error as e:
logging.warning("BLTouch raise probe error: %s", str(e))
def sync_mcu_print_time(self):
curtime = self.printer.get_reactor().monotonic()
@ -128,7 +127,8 @@ class BLTouchEndstopWrapper:
# The "probe raised" test completed successfully
break
if retry >= 2:
raise homing.EndstopError("BLTouch failed to raise probe")
raise self.printer.command_error(
"BLTouch failed to raise probe")
msg = "Failed to verify BLTouch probe is raised; retrying."
self.gcode.respond_info(msg)
self.sync_mcu_print_time()
@ -161,7 +161,7 @@ class BLTouchEndstopWrapper:
return
msg = "BLTouch failed to verify sensor state"
if retry >= 2:
raise homing.EndstopError(msg)
raise self.printer.command_error(msg)
self.gcode.respond_info(msg + '; retrying.')
self.send_cmd('reset', duration=RETRY_RESET_TIME)
def multi_probe_begin(self):
@ -191,7 +191,7 @@ class BLTouchEndstopWrapper:
# Verify the probe actually deployed during the attempt
for s, mcu_pos in self.start_mcu_pos:
if s.get_mcu_position() == mcu_pos:
raise homing.EndstopError("BLTouch failed to deploy")
raise self.printer.command_error("BLTouch failed to deploy")
def home_start(self, print_time, sample_time, sample_count, rest_time):
rest_time = min(rest_time, ENDSTOP_REST_TIME)
return self.mcu_endstop.home_start(print_time, sample_time,

View File

@ -62,7 +62,7 @@ class MCU_buttons:
ack_diff -= 0x100
msg_ack_count = ack_count - ack_diff
# Determine new buttons
buttons = params['state']
buttons = bytearray(params['state'])
new_count = msg_ack_count + len(buttons) - self.ack_count
if new_count <= 0:
return
@ -71,9 +71,9 @@ class MCU_buttons:
self.ack_cmd.send([self.oid, new_count])
self.ack_count += new_count
# Call self.handle_button() with this event in main thread
for b in new_buttons:
for nb in new_buttons:
self.reactor.register_async_callback(
(lambda e, s=self, b=ord(b): s.handle_button(e, b)))
(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

View File

@ -182,21 +182,25 @@ class DeltaCalibrate:
z_weight = len(distances) / (MEASURE_WEIGHT * len(probe_positions))
# Perform coordinate descent
def delta_errorfunc(params):
# Build new delta_params for params under test
delta_params = orig_delta_params.new_calibration(params)
# Calculate z height errors
total_error = 0.
for z_offset, stable_pos in height_positions:
x, y, z = delta_params.get_position_from_stable(stable_pos)
total_error += (z - z_offset)**2
total_error *= z_weight
# Calculate distance errors
for dist, stable_pos1, stable_pos2 in distances:
x1, y1, z1 = delta_params.get_position_from_stable(stable_pos1)
x2, y2, z2 = delta_params.get_position_from_stable(stable_pos2)
d = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
total_error += (d - dist)**2
return total_error
try:
# Build new delta_params for params under test
delta_params = orig_delta_params.new_calibration(params)
getpos = delta_params.get_position_from_stable
# Calculate z height errors
total_error = 0.
for z_offset, stable_pos in height_positions:
x, y, z = getpos(stable_pos)
total_error += (z - z_offset)**2
total_error *= z_weight
# Calculate distance errors
for dist, stable_pos1, stable_pos2 in distances:
x1, y1, z1 = getpos(stable_pos1)
x2, y2, z2 = getpos(stable_pos2)
d = math.sqrt((x1-x2)**2 + (y1-y2)**2 + (z1-z2)**2)
total_error += (d - dist)**2
return total_error
except ValueError:
return 9999999999999.9
new_params = mathutil.background_coordinate_descent(
self.printer, adj_params, params, delta_errorfunc)
# Log and report results

View File

@ -79,6 +79,7 @@ class DisplayGroup:
for row, col, template in self.data_items:
text = template.render(context)
display.draw_text(row, col, text.replace('\n', ''), eventtime)
context.clear() # Remove circular references for better gc
class PrinterLCD:
def __init__(self, config):

View File

@ -103,7 +103,7 @@ class HD44780:
data = self.icons.get(glyph_name)
if data is not None:
slot, bits = data
self.write_text(x, y, chr(slot))
self.write_text(x, y, [slot])
self.glyph_framebuffer[slot * 8:(slot + 1) * 8] = bits
return 1
char = TextGlyphs.get(glyph_name)

View File

@ -397,8 +397,8 @@ class MenuContainer(MenuElement):
return self.select_at(index)
# override
def render_container(self, eventtime):
return ("", None)
def render_container(self, nrows, eventtime):
return []
def __iter__(self):
return iter(self._items)
@ -550,6 +550,7 @@ class MenuInput(MenuCommand):
class MenuList(MenuContainer):
def __init__(self, manager, config):
super(MenuList, self).__init__(manager, config)
self._viewport_top = 0
# create back item
self._itemBack = self.manager.menuitem_from({
'type': 'command',
@ -562,29 +563,43 @@ class MenuList(MenuContainer):
def _populate(self):
super(MenuList, self)._populate()
self._viewport_top = 0
# add back as first item
self.insert_item(self._itemBack, 0)
def render_container(self, eventtime):
rows = []
selected_row = None
def render_container(self, nrows, eventtime):
manager = self.manager
lines = []
selected_row = self.selected
# adjust viewport
if selected_row is not None:
if selected_row >= (self._viewport_top + nrows):
self._viewport_top = (selected_row - nrows) + 1
if selected_row < self._viewport_top:
self._viewport_top = selected_row
else:
self._viewport_top = 0
# clamps viewport
self._viewport_top = max(0, min(self._viewport_top, len(self) - nrows))
try:
for row, item in enumerate(self):
for row in range(self._viewport_top, self._viewport_top + nrows):
s = ""
selected = (row == self.selected)
if selected:
item.heartbeat(eventtime)
selected_row = len(rows)
name = str(item.render_name(selected))
if isinstance(item, MenuList):
s += name[:self.manager.cols-1].ljust(self.manager.cols-1)
s += '>'
else:
s += name[:self.manager.cols].ljust(self.manager.cols)
rows.append(s)
if row < len(self):
current = self[row]
selected = (row == selected_row)
if selected:
current.heartbeat(eventtime)
name = manager.stripliterals(
manager.aslatin(current.render_name(selected)))
if isinstance(current, MenuList):
s += name[:manager.cols-1].ljust(manager.cols-1)
s += '>'
else:
s += name
lines.append(s[:manager.cols].ljust(manager.cols))
except Exception:
logging.exception('List rendering error')
return ("\n".join(rows), selected_row)
return lines
class MenuVSDList(MenuList):
@ -624,7 +639,6 @@ class MenuManager:
self.menuitems = {}
self.menustack = []
self.children = {}
self.top_row = 0
self.display = display
self.printer = config.get_printer()
self.pconfig = self.printer.lookup_object('configfile')
@ -682,7 +696,6 @@ class MenuManager:
def begin(self, eventtime):
self.menustack = []
self.top_row = 0
self.timer = 0
if isinstance(self.root, MenuContainer):
# send begin event
@ -780,21 +793,7 @@ class MenuManager:
container = self.stack_peek()
if self.running and isinstance(container, MenuContainer):
container.heartbeat(eventtime)
content, viewport_row = container.render_container(eventtime)
if viewport_row is not None:
while viewport_row >= (self.top_row + self.rows):
self.top_row += 1
while viewport_row < self.top_row and self.top_row > 0:
self.top_row -= 1
else:
self.top_row = 0
rows = self.aslatin(content).splitlines()
for row in range(0, self.rows):
try:
text = self.stripliterals(rows[self.top_row + row])
except IndexError:
text = ""
lines.append(text.ljust(self.cols))
lines = container.render_container(self.rows, eventtime)
return lines
def screen_update_event(self, eventtime):
@ -884,6 +883,10 @@ class MenuManager:
current = container.selected_item()
if isinstance(current, MenuContainer):
self.stack_push(current)
elif isinstance(current, MenuInput):
if current.is_editing():
current.run_script('gcode', event=event)
current.run_script(event)
elif isinstance(current, MenuCommand):
current.run_script('gcode', event=event)
current.run_script(event)

View File

@ -13,10 +13,11 @@ BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
TextGlyphs = { 'right_arrow': '\x1a', 'degrees': '\xf8' }
class DisplayBase:
def __init__(self, io, columns=128):
def __init__(self, io, columns=128, x_offset=0):
self.send = io.send
# framebuffers
self.columns = columns
self.x_offset = x_offset
self.vram = [bytearray(self.columns) for i in range(8)]
self.all_framebuffers = [(self.vram[i], bytearray('~'*self.columns), i)
for i in range(8)]
@ -71,10 +72,11 @@ class DisplayBase:
if x + len(data) > 16:
data = data[:16 - min(x, 16)]
pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1]
for c in data:
bits_top, bits_bot = self.font[ord(c)]
for c in bytearray(data):
bits_top, bits_bot = self.font[c]
page_top[pix_x:pix_x+8] = bits_top
page_bot[pix_x:pix_x+8] = bits_bot
pix_x += 8
@ -83,6 +85,7 @@ class DisplayBase:
return
bits_top, bits_bot = self._swizzle_bits(data)
pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1]
for i in range(8):
@ -93,6 +96,7 @@ class DisplayBase:
if icon is not None and x < 15:
# Draw icon in graphics mode
pix_x = x * 8
pix_x += self.x_offset
page_idx = y * 2
self.vram[page_idx][pix_x:pix_x+16] = icon[0]
self.vram[page_idx + 1][pix_x:pix_x+16] = icon[1]
@ -192,7 +196,7 @@ class UC1701(DisplayBase):
# The SSD1306 supports both i2c and "4-wire" spi
class SSD1306(DisplayBase):
def __init__(self, config, columns=128):
def __init__(self, config, columns=128, x_offset=0):
cs_pin = config.get("cs_pin", None)
if cs_pin is None:
io = I2C(config, 60)
@ -201,7 +205,7 @@ class SSD1306(DisplayBase):
io = SPI4wire(config, "dc_pin")
io_bus = io.spi
self.reset = ResetHelper(config.get("reset_pin", None), io_bus)
DisplayBase.__init__(self, io, columns)
DisplayBase.__init__(self, io, columns, x_offset)
self.contrast = config.getint('contrast', 239, minval=0, maxval=255)
self.vcomh = config.getint('vcomh', 0, minval=0, maxval=63)
self.invert = config.getboolean('invert', False)
@ -232,4 +236,5 @@ class SSD1306(DisplayBase):
# the SH1106 is SSD1306 compatible with up to 132 columns
class SH1106(SSD1306):
def __init__(self, config):
SSD1306.__init__(self, config, 132)
x_offset = config.getint('x_offset', 0, minval=0, maxval=3)
SSD1306.__init__(self, config, 132, x_offset=x_offset)

View File

@ -4,7 +4,6 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging
import homing
TRINAMIC_DRIVERS = ["tmc2130", "tmc2208", "tmc2209", "tmc2660", "tmc5160"]
@ -81,7 +80,7 @@ class EndstopPhase:
except Exception as e:
msg = "Unable to get stepper %s phase: %s" % (self.name, str(e))
logging.exception(msg)
raise homing.EndstopError(msg)
raise self.printer.command_error(msg)
if stepper.is_dir_inverted():
phase = (self.phases - 1) - phase
else:
@ -95,7 +94,7 @@ class EndstopPhase:
if delta >= self.phases - self.endstop_phase_accuracy:
delta -= self.phases
elif delta > self.endstop_phase_accuracy:
raise homing.EndstopError(
raise self.printer.command_error(
"Endstop %s incorrect phase (got %d vs %d)" % (
self.name, phase, self.endstop_phase))
return delta * self.step_dist

View File

@ -12,7 +12,17 @@ class GCodeButton:
self.pin = config.get('pin')
self.last_state = 0
buttons = self.printer.load_object(config, "buttons")
buttons.register_buttons([self.pin], self.button_callback)
analog_range = config.get('analog_range', None)
if analog_range is None:
buttons.register_buttons([self.pin], self.button_callback)
else:
try:
amin, amax = map(float, analog_range.split(','))
except:
raise config.error("Unable to parse analog_range")
pullup = config.getfloat('analog_pullup_resistor', 4700., above=0.)
buttons.register_adc_button(self.pin, amin, amax, pullup,
self.button_callback)
gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.press_template = gcode_macro.load_template(config, 'press_gcode')
self.release_template = gcode_macro.load_template(config,

View File

@ -74,10 +74,10 @@ class Heater:
pwm_time = read_time + self.pwm_delay
self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME
self.last_pwm_value = value
logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])",
self.name, value, pwm_time,
self.last_temp, self.last_temp_time, self.target_temp)
self.mcu_pwm.set_pwm(pwm_time, value)
#logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])",
# self.name, value, pwm_time,
# self.last_temp, self.last_temp_time, self.target_temp)
def temperature_callback(self, read_time, temp):
with self.lock:
time_diff = read_time - self.last_temp_time

View File

@ -124,7 +124,7 @@ class InputShaper:
self.shapers.keys()[
self.shapers.values().index(shaper_type_x)]
, self.shapers.keys()[
self.shapers.values().index(shaper_type_x)]
self.shapers.values().index(shaper_type_y)]
, shaper_freq_x, shaper_freq_y
, damping_ratio_x, damping_ratio_y))

View File

@ -4,7 +4,6 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, bisect
import homing
class ManualProbe:
def __init__(self, config):
@ -97,7 +96,7 @@ class ManualProbeHelper:
if curpos[2] < z_bob_pos:
self.toolhead.manual_move([None, None, z_bob_pos], self.speed)
self.toolhead.manual_move([None, None, z_pos], self.speed)
except homing.CommandError as e:
except self.printer.command_error as e:
self.finalize(False)
raise
def report_z_status(self, warn_no_change=False, prev_pos=None):

View File

@ -3,7 +3,7 @@
# Copyright (C) 2019 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import stepper, homing, chelper
import stepper, chelper
from . import force_move
ENDSTOP_SAMPLE_TIME = .000015
@ -110,7 +110,7 @@ class ManualStepper:
error = str(e)
self.sync_print_time()
if error is not None:
raise homing.CommandError(error)
raise self.printer.command_error(error)
cmd_MANUAL_STEPPER_help = "Command a manually configured stepper"
def cmd_MANUAL_STEPPER(self, gcmd):
enable = gcmd.get_int('ENABLE', None)

View File

@ -130,7 +130,7 @@ class ControlAutoTune:
def calc_final_pid(self):
cycle_times = [(self.peaks[pos][1] - self.peaks[pos-2][1], pos)
for pos in range(4, len(self.peaks))]
midpoint_pos = sorted(cycle_times)[len(cycle_times)/2][1]
midpoint_pos = sorted(cycle_times)[len(cycle_times)//2][1]
return self.calc_pid(midpoint_pos)
# Offline analysis helper
def write_file(self, filename):

View File

@ -8,10 +8,9 @@ import pins, homing
from . import manual_probe
HINT_TIMEOUT = """
Make sure to home the printer before probing. 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).
If the probe did not move far enough to trigger, then
consider reducing the Z axis minimum position so the probe
can travel further (the Z minimum position can be negative).
"""
class PrinterProbe:
@ -26,6 +25,7 @@ class PrinterProbe:
self.z_offset = config.getfloat('z_offset')
self.probe_calibrate_z = 0.
self.multi_probe_pending = False
self.last_state = False
# Infer Z position to move to during a probe
if config.has_section('stepper_z'):
zconfig = config.getsection('stepper_z')
@ -107,6 +107,9 @@ class PrinterProbe:
return self.x_offset, self.y_offset, self.z_offset
def _probe(self, speed):
toolhead = self.printer.lookup_object('toolhead')
curtime = self.printer.get_reactor().monotonic()
if 'z' not in toolhead.get_status(curtime)['homed_axes']:
raise self.printer.command_error("Must home before probe")
homing_state = homing.Homing(self.printer)
pos = toolhead.get_position()
pos[2] = self.z_position
@ -115,11 +118,11 @@ class PrinterProbe:
try:
homing_state.homing_move(pos, endstops, speed,
probe_pos=True, verify_movement=verify)
except homing.CommandError as e:
except self.printer.command_error as e:
reason = str(e)
if "Timeout during endstop homing" in reason:
reason += HINT_TIMEOUT
raise homing.CommandError(reason)
raise self.printer.command_error(reason)
pos = toolhead.get_position()
self.gcode.respond_info("probe at %.3f,%.3f is z=%.6f"
% (pos[0], pos[1], pos[2]))
@ -162,8 +165,7 @@ class PrinterProbe:
z_positions = [p[2] for p in positions]
if max(z_positions) - min(z_positions) > samples_tolerance:
if retries >= samples_retries:
raise homing.CommandError(
"Probe samples exceed samples_tolerance")
raise gcmd.error("Probe samples exceed samples_tolerance")
gcmd.respond_info("Probe samples exceed tolerance. Retrying...")
retries += 1
positions = []
@ -186,7 +188,10 @@ class PrinterProbe:
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
res = self.mcu_probe.query_endstop(print_time)
self.last_state = res
gcmd.respond_info("probe: %s" % (["open", "TRIGGERED"][not not res],))
def get_status(self, eventtime):
return {'last_query': self.last_state}
cmd_PROBE_ACCURACY_help = "Probe Z-height accuracy at current XY position"
def cmd_PROBE_ACCURACY(self, gcmd):
speed = gcmd.get_float("PROBE_SPEED", self.speed, above=0.)
@ -295,14 +300,14 @@ class ProbeEndstopWrapper:
start_pos = toolhead.get_position()
self.activate_gcode.run_gcode_from_command()
if toolhead.get_position()[:3] != start_pos[:3]:
raise homing.CommandError(
raise self.printer.command_error(
"Toolhead moved during probe activate_gcode script")
def probe_finish(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 homing.CommandError(
raise self.printer.command_error(
"Toolhead moved during probe deactivate_gcode script")
def get_position_endstop(self):
return self.position_endstop
@ -337,6 +342,9 @@ class ProbePointsHelper:
if len(self.probe_points) < n:
raise self.printer.config_error(
"Need at least %d probe points for %s" % (n, self.name))
def update_probe_points(self, points, min_points):
self.probe_points = points
self.minimum_points(min_points)
def use_xy_offsets(self, use_offsets):
self.use_offsets = use_offsets
def get_lift_speed(self):

View File

@ -6,6 +6,22 @@
import logging
from . import probe, z_tilt
# Leveling code for XY rails that are controlled by Z steppers as in:
#
# Z stepper1 ----> O O <---- Z stepper2
# | * <-- probe1 probe2 --> * |
# | |
# | | <--- Y2 rail
# Y1 rail -----> | |
# | |
# |=============================|
# | ^ |
# | | |
# | X rail --/ |
# | |
# | * <-- probe0 probe3 --> * |
# Z stepper0 ----> O O <---- Z stepper3
class QuadGantryLevel:
def __init__(self, config):
self.printer = config.get_printer()
@ -48,29 +64,36 @@ class QuadGantryLevel:
" ".join(["%s: %.6f" % (z_id, z_positions[z_id])
for z_id in range(len(z_positions))]))
self.gcode.respond_info(points_message)
p1 = [positions[0][0] + offsets[0],z_positions[0]]
p2 = [positions[1][0] + offsets[0],z_positions[1]]
p3 = [positions[2][0] + offsets[0],z_positions[2]]
p4 = [positions[3][0] + offsets[0],z_positions[3]]
f1 = self.linefit(p1,p4)
f2 = self.linefit(p2,p3)
logging.info("quad_gantry_level f1: %s, f2: %s" % (f1,f2))
# Calculate slope along X axis between probe point 0 and 3
ppx0 = [positions[0][0] + offsets[0], z_positions[0]]
ppx3 = [positions[3][0] + offsets[0], z_positions[3]]
slope_x_pp03 = self.linefit(ppx0, ppx3)
# Calculate slope along X axis between probe point 1 and 2
ppx1 = [positions[1][0] + offsets[0], z_positions[1]]
ppx2 = [positions[2][0] + offsets[0], z_positions[2]]
slope_x_pp12 = self.linefit(ppx1, ppx2)
logging.info("quad_gantry_level f1: %s, f2: %s"
% (slope_x_pp03, slope_x_pp12))
# Calculate gantry slope along Y axis between stepper 0 and 1
a1 = [positions[0][1] + offsets[1],
self.plot(f1,self.gantry_corners[0][0])]
self.plot(slope_x_pp03, self.gantry_corners[0][0])]
a2 = [positions[1][1] + offsets[1],
self.plot(f2,self.gantry_corners[0][0])]
self.plot(slope_x_pp12, self.gantry_corners[0][0])]
slope_y_s01 = self.linefit(a1, a2)
# Calculate gantry slope along Y axis between stepper 2 and 3
b1 = [positions[0][1] + offsets[1],
self.plot(f1,self.gantry_corners[1][0])]
self.plot(slope_x_pp03, self.gantry_corners[1][0])]
b2 = [positions[1][1] + offsets[1],
self.plot(f2,self.gantry_corners[1][0])]
af = self.linefit(a1,a2)
bf = self.linefit(b1,b2)
logging.info("quad_gantry_level af: %s, bf: %s" % (af,bf))
self.plot(slope_x_pp12, self.gantry_corners[1][0])]
slope_y_s23 = self.linefit(b1, b2)
logging.info("quad_gantry_level af: %s, bf: %s"
% (slope_y_s01, slope_y_s23))
# Calculate z height of each stepper
z_height = [0,0,0,0]
z_height[0] = self.plot(af,self.gantry_corners[0][1])
z_height[1] = self.plot(af,self.gantry_corners[1][1])
z_height[2] = self.plot(bf,self.gantry_corners[1][1])
z_height[3] = self.plot(bf,self.gantry_corners[0][1])
z_height[0] = self.plot(slope_y_s01, self.gantry_corners[0][1])
z_height[1] = self.plot(slope_y_s01, self.gantry_corners[1][1])
z_height[2] = self.plot(slope_y_s23, self.gantry_corners[1][1])
z_height[3] = self.plot(slope_y_s23, self.gantry_corners[0][1])
ainfo = zip(["z","z1","z2","z3"], z_height[0:4])
apos = " ".join(["%s: %06f" % (x) for x in ainfo])

View File

@ -267,15 +267,18 @@ MAX31865_FAULT_REFINHIGH = 0x10
MAX31865_FAULT_RTDINLOW = 0x08
MAX31865_FAULT_OVUV = 0x04
VAL_A = 0.00390830
VAL_B = 0.0000005775
VAL_C = -0.00000000000418301
VAL_ADC_MAX = 32768.0 # 2^15
MAX31865_ADC_MAX = 1<<15
# Callendar-Van Dusen constants for platinum resistance thermometers (RTD)
CVD_A = 3.9083e-3
CVD_B = -5.775e-7
class MAX31865(SensorBase):
def __init__(self, config):
self.rtd_nominal_r = config.getint('rtd_nominal_r', 100)
self.reference_r = config.getfloat('rtd_reference_r', 430., above=0.)
rtd_nominal_r = config.getfloat('rtd_nominal_r', 100., above=0.)
rtd_reference_r = config.getfloat('rtd_reference_r', 430., above=0.)
adc_to_resist = rtd_reference_r / float(MAX31865_ADC_MAX)
self.adc_to_resist_div_nominal = adc_to_resist / rtd_nominal_r
SensorBase.__init__(self, config, "MAX31865",
self.build_spi_init(config))
def calc_temp(self, adc, fault):
@ -295,20 +298,20 @@ class MAX31865(SensorBase):
if fault & 0x03:
self.fault("Max31865 Unspecified error")
adc = adc >> 1 # remove fault bit
R_rtd = (self.reference_r * adc) / VAL_ADC_MAX
temp = ((( ( -1 * self.rtd_nominal_r ) * VAL_A )
+ math.sqrt( ( self.rtd_nominal_r**2 * VAL_A * VAL_A )
- ( 4 * self.rtd_nominal_r * VAL_B
* ( self.rtd_nominal_r - R_rtd ) )))
/ (2 * self.rtd_nominal_r * VAL_B))
R_div_nominal = adc * self.adc_to_resist_div_nominal
# Resistance (relative to rtd_nominal_r) is calculated using:
# R_div_nominal = 1. + CVD_A * temp + CVD_B * temp**2
# Solve for temp using quadratic equation:
# temp = (-b +- sqrt(b**2 - 4ac)) / 2a
discriminant = math.sqrt(CVD_A**2 - 4. * CVD_B * (1. - R_div_nominal))
temp = (-CVD_A + discriminant) / (2. * CVD_B)
return temp
def calc_adc(self, temp):
R_rtd = temp * ( 2 * self.rtd_nominal_r * VAL_B )
R_rtd = math.pow( ( R_rtd + ( self.rtd_nominal_r * VAL_A ) ), 2)
R_rtd = -1 * ( R_rtd - (self.rtd_nominal_r**2 * VAL_A * VAL_A ) )
R_rtd = R_rtd / ( 4 * self.rtd_nominal_r * VAL_B )
R_rtd = ( -1 * R_rtd ) + self.rtd_nominal_r
adc = int( ( ( R_rtd * VAL_ADC_MAX ) / self.reference_r) + 0.5 )
# Calculate relative resistance via Callendar-Van Dusen formula:
# resistance = rtd_nominal_r * (1 + CVD_A * temp + CVD_B * temp**2)
R_div_nominal = 1. + CVD_A * temp + CVD_B * temp * temp
adc = int(R_div_nominal / self.adc_to_resist_div_nominal + 0.5)
adc = max(0, min(MAX31865_ADC_MAX, adc))
adc = adc << 1 # Add fault bit
return adc
def build_spi_init(self, config):

View File

@ -251,7 +251,9 @@ class GCodeDispatch:
raise gcmd.error(self.printer.get_state_message()[0])
return
if not cmd:
logging.debug(gcmd.get_commandline())
cmdline = gcmd.get_commandline()
if cmdline:
logging.debug(cmdline)
return
if cmd.startswith("M117 "):
# Handle M117 gcode with numeric and special characters

View File

@ -101,8 +101,8 @@ class Homing:
for s, name, spos, epos in end_mcu_pos:
if spos == epos:
if probe_pos:
raise EndstopError("Probe triggered prior to movement")
raise EndstopError(
raise CommandError("Probe triggered prior to movement")
raise CommandError(
"Endstop %s still triggered after retract" % (name,))
def home_rails(self, rails, forcepos, movepos):
# Notify of upcoming homing operation
@ -161,11 +161,4 @@ def multi_complete(printer, completions):
class CommandError(Exception):
pass
class EndstopError(CommandError):
pass
def EndstopMoveError(pos, msg="Move out of range"):
return EndstopError("%s: %.3f %.3f %.3f [%.3f]" % (
msg, pos[0], pos[1], pos[2], pos[3]))
Coord = collections.namedtuple('Coord', ('x', 'y', 'z', 'e'))

View File

@ -4,7 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging
import stepper, homing
import stepper
class CartKinematics:
def __init__(self, toolhead, config):
@ -100,9 +100,8 @@ class CartKinematics:
and (end_pos[i] < self.limits[i][0]
or end_pos[i] > self.limits[i][1])):
if self.limits[i][0] > self.limits[i][1]:
raise homing.EndstopMoveError(
end_pos, "Must home axis first")
raise homing.EndstopMoveError(end_pos)
raise move.move_error("Must home axis first")
raise move.move_error()
def check_move(self, move):
limits = self.limits
xpos, ypos = move.end_pos[:2]

View File

@ -4,7 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math
import stepper, homing
import stepper
class CoreXYKinematics:
def __init__(self, toolhead, config):
@ -77,9 +77,8 @@ class CoreXYKinematics:
and (end_pos[i] < self.limits[i][0]
or end_pos[i] > self.limits[i][1])):
if self.limits[i][0] > self.limits[i][1]:
raise homing.EndstopMoveError(
end_pos, "Must home axis first")
raise homing.EndstopMoveError(end_pos)
raise move.move_error("Must home axis first")
raise move.move_error()
def check_move(self, move):
limits = self.limits
xpos, ypos = move.end_pos[:2]

View File

@ -4,7 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math
import stepper, homing
import stepper
class CoreXZKinematics:
def __init__(self, toolhead, config):
@ -76,9 +76,8 @@ class CoreXZKinematics:
and (end_pos[i] < self.limits[i][0]
or end_pos[i] > self.limits[i][1])):
if self.limits[i][0] > self.limits[i][1]:
raise homing.EndstopMoveError(
end_pos, "Must home axis first")
raise homing.EndstopMoveError(end_pos)
raise move.move_error("Must home axis first")
raise move.move_error()
def check_move(self, move):
limits = self.limits
xpos, ypos = move.end_pos[:2]

View File

@ -4,7 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging
import stepper, homing, mathutil
import stepper, mathutil
# Slow moves once the ratio of tower to XY movement exceeds SLOW_RATIO
SLOW_RATIO = 3.
@ -118,7 +118,7 @@ class DeltaKinematics:
# Normal XY move
return
if self.need_home:
raise homing.EndstopMoveError(end_pos, "Must home first")
raise move.move_error("Must home first")
end_z = end_pos[2]
limit_xy2 = self.max_xy2
if end_z > self.limit_z:
@ -127,7 +127,7 @@ class DeltaKinematics:
# Move out of range - verify not a homing move
if (end_pos[:2] != self.home_position[:2]
or end_z < self.min_z or end_z > self.home_position[2]):
raise homing.EndstopMoveError(end_pos)
raise move.move_error()
limit_xy2 = -1.
if move.axes_d[2]:
move.limit_speed(self.max_z_velocity, move.accel)

View File

@ -4,7 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging
import stepper, homing, chelper
import stepper, chelper
class PrinterExtruder:
def __init__(self, config, extruder_num):
@ -107,13 +107,13 @@ class PrinterExtruder:
def check_move(self, move):
axis_r = move.axes_r[3]
if not self.heater.can_extrude:
raise homing.EndstopError(
raise self.printer.command_error(
"Extrude below minimum temp\n"
"See the 'min_extrude_temp' config option for details")
if (not move.axes_d[0] and not move.axes_d[1]) or axis_r < 0.:
# Extrude only move (or retraction move) - limit accel and velocity
if abs(move.axes_d[3]) > self.max_e_dist:
raise homing.EndstopError(
raise self.printer.command_error(
"Extrude only move too long (%.3fmm vs %.3fmm)\n"
"See the 'max_extrude_only_distance' config"
" option for details" % (move.axes_d[3], self.max_e_dist))
@ -127,7 +127,7 @@ class PrinterExtruder:
area = axis_r * self.filament_area
logging.debug("Overextrude: %s vs %s (area=%.3f dist=%.3f)",
axis_r, self.max_extrude_ratio, area, move.move_d)
raise homing.EndstopError(
raise self.printer.command_error(
"Move exceeds maximum extrusion (%.3fmm^2 vs %.3fmm^2)\n"
"See the 'max_extrude_cross_section' config option for details"
% (area, self.max_extrude_ratio * self.filament_area))
@ -214,17 +214,18 @@ class PrinterExtruder:
# Dummy extruder class used when a printer has no extruder at all
class DummyExtruder:
def __init__(self, printer):
self.printer = printer
def update_move_time(self, flush_time):
pass
def check_move(self, move):
raise homing.EndstopMoveError(
move.end_pos, "Extrude when no extruder present")
raise move.move_error("Extrude when no extruder present")
def calc_junction(self, prev_move, move):
return move.max_cruise_v2
def get_name(self):
return ""
def get_heater(self):
raise homing.CommandError("Extruder not configured")
raise self.printer.command_error("Extruder not configured")
def add_printer_objects(config):
printer = config.get_printer()

View File

@ -4,7 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math
import stepper, homing
import stepper
class PolarKinematics:
def __init__(self, toolhead, config):
@ -94,18 +94,17 @@ class PolarKinematics:
xy2 = end_pos[0]**2 + end_pos[1]**2
if xy2 > self.limit_xy2:
if self.limit_xy2 < 0.:
raise homing.EndstopMoveError(end_pos, "Must home axis first")
raise homing.EndstopMoveError(end_pos)
raise move.move_error("Must home axis first")
raise move.move_error()
if move.axes_d[2]:
if end_pos[2] < self.limit_z[0] or end_pos[2] > self.limit_z[1]:
if self.limit_z[0] > self.limit_z[1]:
raise homing.EndstopMoveError(
end_pos, "Must home axis first")
raise homing.EndstopMoveError(end_pos)
raise move.move_error("Must home axis first")
raise move.move_error()
# Move with Z - update velocity and accel for slower Z axis
z_ratio = move.move_d / abs(move.axes_d[2])
move.limit_speed(
self.max_z_velocity * z_ratio, self.max_z_accel * z_ratio)
move.limit_speed(self.max_z_velocity * z_ratio,
self.max_z_accel * z_ratio)
def get_status(self, eventtime):
xy_home = "xy" if self.limit_xy2 >= 0. else ""
z_home = "z" if self.limit_z[0] <= self.limit_z[1] else ""

View File

@ -4,7 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging
import stepper, homing, mathutil, chelper
import stepper, mathutil, chelper
class RotaryDeltaKinematics:
def __init__(self, toolhead, config):
@ -106,7 +106,7 @@ class RotaryDeltaKinematics:
# Normal XY move
return
if self.need_home:
raise homing.EndstopMoveError(end_pos, "Must home first")
raise move.move_error("Must home first")
end_z = end_pos[2]
limit_xy2 = self.max_xy2
if end_z > self.limit_z:
@ -115,7 +115,7 @@ class RotaryDeltaKinematics:
# Move out of range - verify not a homing move
if (end_pos[:2] != self.home_position[:2]
or end_z < self.min_z or end_z > self.home_position[2]):
raise homing.EndstopMoveError(end_pos)
raise move.move_error()
limit_xy2 = -1.
if move.axes_d[2]:
move.limit_speed(self.max_z_velocity, move.accel)

View File

@ -1,10 +1,10 @@
#!/usr/bin/env python2
# Main code for host side printer firmware
#
# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys, os, optparse, logging, time, threading, collections, importlib
import sys, os, gc, optparse, logging, time, threading, collections, importlib
import util, reactor, queuelogger, msgproto, homing
import gcode, configfile, pins, mcu, toolhead, webhooks
@ -48,10 +48,10 @@ Printer is shutdown
class Printer:
config_error = configfile.error
command_error = homing.CommandError
def __init__(self, bglogger, start_args):
def __init__(self, main_reactor, bglogger, start_args):
self.bglogger = bglogger
self.start_args = start_args
self.reactor = reactor.Reactor()
self.reactor = main_reactor
self.reactor.register_callback(self._connect)
self.state_message = message_startup
self.in_shutdown_state = False
@ -165,8 +165,8 @@ class Printer:
return
except Exception as e:
logging.exception("Unhandled exception during connect")
self._set_state("Internal error during connect: %s\n%s" % (
str(e), message_restart,))
self._set_state("Internal error during connect: %s\n%s"
% (str(e), message_restart,))
return
try:
self._set_state(message_ready)
@ -176,8 +176,8 @@ class Printer:
cb()
except Exception as e:
logging.exception("Unhandled exception during ready callback")
self.invoke_shutdown("Internal error during ready callback: %s" % (
str(e),))
self.invoke_shutdown("Internal error during ready callback: %s"
% (str(e),))
def run(self):
systime = time.time()
monotime = self.reactor.monotonic()
@ -187,8 +187,17 @@ class Printer:
try:
self.reactor.run()
except:
logging.exception("Unhandled exception during run")
return "error_exit"
msg = "Unhandled exception during run"
logging.exception(msg)
# Exception from a reactor callback - try to shutdown
try:
self.reactor.register_callback((lambda e:
self.invoke_shutdown(msg)))
self.reactor.run()
except:
logging.exception("Repeat unhandled exception during run")
# Another exception - try to exit
self.run_result = "error_exit"
# Check restart flags
run_result = self.run_result
try:
@ -215,6 +224,8 @@ class Printer:
cb()
except:
logging.exception("Exception during shutdown handler")
logging.info("Reactor garbage collection: %s",
self.reactor.get_gc_stats())
def invoke_async_shutdown(self, msg):
self.reactor.register_async_callback(
(lambda e: self.invoke_shutdown(msg)))
@ -297,17 +308,22 @@ def main():
elif not options.debugoutput:
logging.warning("No log file specified!"
" Severe timing issues may result!")
gc.disable()
# Start Printer() class
while 1:
if bglogger is not None:
bglogger.clear_rollover_info()
bglogger.set_rollover_info('versions', versions)
printer = Printer(bglogger, start_args)
gc.collect()
main_reactor = reactor.Reactor(gc_checking=True)
printer = Printer(main_reactor, bglogger, start_args)
res = printer.run()
if res in ['exit', 'error_exit']:
break
time.sleep(1.)
main_reactor.finalize()
main_reactor = printer = None
logging.info("Restarting printer")
start_args['start_reason'] = res

View File

@ -213,10 +213,13 @@ class MCU_pwm:
self._start_value * self._pwm_max,
self._shutdown_value * self._pwm_max,
self._mcu.seconds_to_clock(self._max_duration)))
self._mcu.add_config_cmd(
"set_pwm_out pin=%s cycle_ticks=%d value=%d"
% (self._pin, cycle_ticks, self._start_value * self._pwm_max),
on_restart=True)
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.100)
svalue = int(self._start_value * self._pwm_max + 0.5)
self._mcu.add_config_cmd("schedule_pwm_out oid=%d clock=%d value=%d"
% (self._oid, self._last_clock, svalue),
on_restart=True)
self._set_cmd = self._mcu.lookup_command(
"schedule_pwm_out oid=%c clock=%u value=%hu", cq=cmd_queue)
return
@ -427,8 +430,8 @@ class MCU:
self._mcu_freq = 0.
# Move command queuing
ffi_main, self._ffi_lib = chelper.get_ffi()
self._max_stepper_error = config.getfloat(
'max_stepper_error', 0.000025, minval=0.)
self._max_stepper_error = config.getfloat('max_stepper_error', 0.000025,
minval=0.)
self._stepqueues = []
self._steppersync = None
# Stats
@ -563,17 +566,19 @@ class MCU:
else:
start_reason = self._printer.get_start_args().get("start_reason")
if start_reason == 'firmware_restart':
raise error("Failed automated reset of MCU '%s'" % (
self._name,))
raise error("Failed automated reset of MCU '%s'"
% (self._name,))
# Already configured - send init commands
self._send_config(config_params['crc'])
# Setup steppersync with the move_count returned by get_config
move_count = config_params['move_count']
self._steppersync = self._ffi_lib.steppersync_alloc(
self._serial.serialqueue, self._stepqueues, len(self._stepqueues),
move_count)
self._ffi_lib.steppersync_set_time(
self._steppersync, 0., self._mcu_freq)
ffi_main, ffi_lib = chelper.get_ffi()
self._steppersync = ffi_main.gc(
ffi_lib.steppersync_alloc(self._serial.serialqueue,
self._stepqueues, len(self._stepqueues),
move_count),
ffi_lib.steppersync_free)
ffi_lib.steppersync_set_time(self._steppersync, 0., self._mcu_freq)
# Log config information
move_msg = "Configured MCU '%s' (%d moves)" % (self._name, move_count)
logging.info(move_msg)
@ -679,9 +684,7 @@ class MCU:
# Restarts
def _disconnect(self):
self._serial.disconnect()
if self._steppersync is not None:
self._ffi_lib.steppersync_free(self._steppersync)
self._steppersync = None
self._steppersync = None
def _shutdown(self, force=False):
if (self._emergency_stop_cmd is None
or (self._is_shutdown and not force)):
@ -742,8 +745,8 @@ class MCU:
return
ret = self._ffi_lib.steppersync_flush(self._steppersync, clock)
if ret:
raise error("Internal error in MCU '%s' stepcompress" % (
self._name,))
raise error("Internal error in MCU '%s' stepcompress"
% (self._name,))
def check_active(self, print_time, eventtime):
if self._steppersync is None:
return
@ -763,8 +766,6 @@ class MCU:
self._mcu_tick_stddev)
return False, ' '.join([msg, self._serial.stats(eventtime),
self._clocksync.stats(eventtime)])
def __del__(self):
self._disconnect()
Common_MCU_errors = {
("Timer too close", "No next step", "Missed scheduling of next "): """

View File

@ -39,6 +39,9 @@ class QueueListener(logging.handlers.TimedRotatingFileHandler):
self.bg_queue.put_nowait(None)
self.bg_thread.join()
def set_rollover_info(self, name, info):
if info is None:
self.rollover_info.pop(name, None)
return
self.rollover_info[name] = info
def clear_rollover_info(self):
self.rollover_info.clear()

View File

@ -1,9 +1,9 @@
# File descriptor and timer event helper
#
# Copyright (C) 2016-2019 Kevin O'Connor <kevin@koconnor.net>
# Copyright (C) 2016-2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import os, select, math, time, Queue as queue
import os, gc, select, math, time, logging, Queue as queue
import greenlet
import chelper, util
@ -93,10 +93,13 @@ class ReactorMutex:
class SelectReactor:
NOW = _NOW
NEVER = _NEVER
def __init__(self):
def __init__(self, gc_checking=False):
# Main code
self._process = False
self.monotonic = chelper.get_ffi()[1].get_monotonic
# Python garbage collection
self._check_gc = gc_checking
self._last_gc_times = [0., 0., 0.]
# Timers
self._timers = []
self._next_timer = self.NEVER
@ -108,6 +111,9 @@ class SelectReactor:
# Greenlets
self._g_dispatch = None
self._greenlets = []
self._all_greenlets = []
def get_gc_stats(self):
return tuple(self._last_gc_times)
# Timers
def update_timer(self, timer_handler, waketime):
timer_handler.waketime = waketime
@ -124,8 +130,22 @@ class SelectReactor:
timers = list(self._timers)
timers.pop(timers.index(timer_handler))
self._timers = timers
def _check_timers(self, eventtime):
def _check_timers(self, eventtime, busy):
if eventtime < self._next_timer:
if busy:
return 0.
if self._check_gc:
gi = gc.get_count()
if gi[0] >= 700:
# Reactor looks idle and gc is due - run it
gc_level = 0
if gi[1] >= 10:
gc_level = 1
if gi[2] >= 10:
gc_level = 2
self._last_gc_times[gc_level] = eventtime
gc.collect(gc_level)
return 0.
return min(1., max(.001, self._next_timer - eventtime))
self._next_timer = self.NEVER
g_dispatch = self._g_dispatch
@ -139,9 +159,7 @@ class SelectReactor:
self._end_greenlet(g_dispatch)
return 0.
self._next_timer = min(self._next_timer, waketime)
if eventtime >= self._next_timer:
return 0.
return min(1., max(.001, self._next_timer - self.monotonic()))
return 0.
# Callbacks and Completions
def completion(self):
return ReactorCompletion(self)
@ -178,11 +196,6 @@ class SelectReactor:
util.set_nonblock(self._pipe_fds[0])
util.set_nonblock(self._pipe_fds[1])
self.register_fd(self._pipe_fds[0], self._got_pipe_signal)
def __del__(self):
if self._pipe_fds is not None:
os.close(self._pipe_fds[0])
os.close(self._pipe_fds[1])
self._pipe_fds = None
# Greenlets
def _sys_pause(self, waketime):
# Pause using system sleep for when reactor not running
@ -202,6 +215,7 @@ class SelectReactor:
g_next = self._greenlets.pop()
else:
g_next = ReactorGreenlet(run=self._dispatch_loop)
self._all_greenlets.append(g_next)
g_next.parent = g.parent
g.timer = self.register_timer(g.switch, waketime)
self._next_timer = self.NOW
@ -231,12 +245,15 @@ class SelectReactor:
# Main loop
def _dispatch_loop(self):
self._g_dispatch = g_dispatch = greenlet.getcurrent()
busy = True
eventtime = self.monotonic()
while self._process:
timeout = self._check_timers(eventtime)
timeout = self._check_timers(eventtime, busy)
busy = False
res = select.select(self._fds, [], [], timeout)
eventtime = self.monotonic()
for fd in res[0]:
busy = True
fd.callback(eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
@ -248,13 +265,27 @@ class SelectReactor:
self._setup_async_callbacks()
self._process = True
g_next = ReactorGreenlet(run=self._dispatch_loop)
self._all_greenlets.append(g_next)
g_next.switch()
def end(self):
self._process = False
def finalize(self):
self._g_dispatch = None
self._greenlets = []
for g in self._all_greenlets:
try:
g.throw()
except:
logging.exception("reactor finalize greenlet terminate")
self._all_greenlets = []
if self._pipe_fds is not None:
os.close(self._pipe_fds[0])
os.close(self._pipe_fds[1])
self._pipe_fds = None
class PollReactor(SelectReactor):
def __init__(self):
SelectReactor.__init__(self)
def __init__(self, gc_checking=False):
SelectReactor.__init__(self, gc_checking)
self._poll = select.poll()
self._fds = {}
# File descriptors
@ -273,12 +304,15 @@ class PollReactor(SelectReactor):
# Main loop
def _dispatch_loop(self):
self._g_dispatch = g_dispatch = greenlet.getcurrent()
busy = True
eventtime = self.monotonic()
while self._process:
timeout = self._check_timers(eventtime)
timeout = self._check_timers(eventtime, busy)
busy = False
res = self._poll.poll(int(math.ceil(timeout * 1000.)))
eventtime = self.monotonic()
for fd, event in res:
busy = True
self._fds[fd](eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)
@ -287,8 +321,8 @@ class PollReactor(SelectReactor):
self._g_dispatch = None
class EPollReactor(SelectReactor):
def __init__(self):
SelectReactor.__init__(self)
def __init__(self, gc_checking=False):
SelectReactor.__init__(self, gc_checking)
self._epoll = select.epoll()
self._fds = {}
# File descriptors
@ -307,12 +341,15 @@ class EPollReactor(SelectReactor):
# Main loop
def _dispatch_loop(self):
self._g_dispatch = g_dispatch = greenlet.getcurrent()
busy = True
eventtime = self.monotonic()
while self._process:
timeout = self._check_timers(eventtime)
timeout = self._check_timers(eventtime, busy)
busy = False
res = self._epoll.poll(timeout)
eventtime = self.monotonic()
for fd, event in res:
busy = True
self._fds[fd](eventtime)
if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch)

View File

@ -98,8 +98,9 @@ class SerialReader:
continue
if self.baud:
stk500v2_leave(self.ser, self.reactor)
self.serialqueue = self.ffi_lib.serialqueue_alloc(
self.ser.fileno(), 0)
self.serialqueue = self.ffi_main.gc(
self.ffi_lib.serialqueue_alloc(self.ser.fileno(), 0),
self.ffi_lib.serialqueue_free)
self.background_thread = threading.Thread(target=self._bg_thread)
self.background_thread.start()
# Obtain and load the data dictionary from the firmware
@ -126,7 +127,9 @@ class SerialReader:
def connect_file(self, debugoutput, dictionary, pace=False):
self.ser = debugoutput
self.msgparser.process_identify(dictionary, decompress=False)
self.serialqueue = self.ffi_lib.serialqueue_alloc(self.ser.fileno(), 1)
self.serialqueue = self.ffi_main.gc(
self.ffi_lib.serialqueue_alloc(self.ser.fileno(), 1),
self.ffi_lib.serialqueue_free)
def set_clock_est(self, freq, last_time, last_clock):
self.ffi_lib.serialqueue_set_clock_est(
self.serialqueue, freq, last_time, last_clock)
@ -135,7 +138,6 @@ class SerialReader:
self.ffi_lib.serialqueue_exit(self.serialqueue)
if self.background_thread is not None:
self.background_thread.join()
self.ffi_lib.serialqueue_free(self.serialqueue)
self.background_thread = self.serialqueue = None
if self.ser is not None:
self.ser.close()
@ -194,10 +196,10 @@ class SerialReader:
self.stats(self.reactor.monotonic()),))
sdata = self.ffi_main.new('struct pull_queue_message[1024]')
rdata = self.ffi_main.new('struct pull_queue_message[1024]')
scount = self.ffi_lib.serialqueue_extract_old(
self.serialqueue, 1, sdata, len(sdata))
rcount = self.ffi_lib.serialqueue_extract_old(
self.serialqueue, 0, rdata, len(rdata))
scount = self.ffi_lib.serialqueue_extract_old(self.serialqueue, 1,
sdata, len(sdata))
rcount = self.ffi_lib.serialqueue_extract_old(self.serialqueue, 0,
rdata, len(rdata))
out.append("Dumping send queue %d messages" % (scount,))
for i in range(scount):
msg = sdata[i]
@ -222,8 +224,6 @@ class SerialReader:
logging.info("%s: %s", params['#name'], params['#msg'])
def handle_default(self, params):
logging.warn("got %s", params)
def __del__(self):
self.disconnect()
# Class to send a query command and return the received response
class SerialRetryCommand:

View File

@ -4,7 +4,7 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging, collections
import homing, chelper
import chelper
class error(Exception):
pass

View File

@ -54,6 +54,10 @@ class Move:
self.accel = min(self.accel, accel)
self.delta_v2 = 2.0 * self.move_d * self.accel
self.smooth_delta_v2 = min(self.smooth_delta_v2, self.delta_v2)
def move_error(self, msg="Move out of range"):
ep = self.end_pos
m = "%s: %.3f %.3f %.3f [%.3f]" % (msg, ep[0], ep[1], ep[2], ep[3])
return self.toolhead.printer.command_error(m)
def calc_junction(self, prev_move):
if not self.is_kinematic_move or not prev_move.is_kinematic_move:
return
@ -246,7 +250,7 @@ class ToolHead:
self.trapq_free_moves = ffi_lib.trapq_free_moves
self.step_generators = []
# Create kinematics class
self.extruder = kinematics.extruder.DummyExtruder()
self.extruder = kinematics.extruder.DummyExtruder(self.printer)
kin_name = config.get('kinematics')
try:
mod = importlib.import_module('kinematics.' + kin_name)
@ -466,7 +470,7 @@ class ToolHead:
# Submit move
try:
self.move(newpos, speed)
except homing.CommandError as e:
except self.printer.command_error as e:
self.flush_step_generation()
raise
# Transmit move in "drip" mode

View File

@ -61,7 +61,8 @@ class WebRequest:
value = self.params.get(item, default)
if value is Sentinel:
raise WebRequestError("Missing Argument [%s]" % (item,))
if types is not None and type(value) not in types:
if (types is not None and type(value) not in types
and item in self.params):
raise WebRequestError("Invalid Argument Type [%s]" % (item,))
return value
@ -168,19 +169,30 @@ class ClientConnection:
self.sock.fileno(), self.process_received)
self.partial_data = self.send_buffer = ""
self.is_sending_data = False
logging.info(
"webhooks: New connection established")
self.set_client_info("?", "New connection")
def set_client_info(self, client_info, state_msg=None):
if state_msg is None:
state_msg = "Client info %s" % (repr(client_info),)
logging.info("webhooks client %s: %s", self.uid, state_msg)
log_id = "webhooks %s" % (self.uid,)
if client_info is None:
self.printer.set_rollover_info(log_id, None, log=False)
return
rollover_msg = "webhooks client %s: %s" % (self.uid, repr(client_info))
self.printer.set_rollover_info(log_id, rollover_msg, log=False)
def close(self):
if self.fd_handle is not None:
logging.info("webhooks: Client connection closed")
self.reactor.unregister_fd(self.fd_handle)
self.fd_handle = None
try:
self.sock.close()
except socket.error:
pass
self.server.pop_client(self.uid)
if self.fd_handle is None:
return
self.set_client_info(None, "Disconnected")
self.reactor.unregister_fd(self.fd_handle)
self.fd_handle = None
try:
self.sock.close()
except socket.error:
pass
self.server.pop_client(self.uid)
def is_closed(self):
return self.fd_handle is None
@ -216,7 +228,7 @@ class ClientConnection:
try:
func = self.webhooks.get_callback(web_request.get_method())
func(web_request)
except homing.CommandError as e:
except self.printer.command_error as e:
web_request.set_error(WebRequestError(e.message))
except Exception as e:
msg = ("Internal Error on WebRequest: %s"
@ -276,9 +288,12 @@ class WebHooks:
web_request.send({'endpoints': self._endpoints.keys()})
def _handle_info_request(self, web_request):
client_info = web_request.get_dict('client_info', None)
if client_info is not None:
web_request.get_client_connection().set_client_info(client_info)
state_message, state = self.printer.get_state_message()
klipper_path = os.path.normpath(os.path.join(
os.path.dirname(__file__), ".."))
src_path = os.path.dirname(__file__)
klipper_path = os.path.normpath(os.path.join(src_path, ".."))
response = {'state': state, 'state_message': state_message,
'hostname': socket.gethostname(),
'klipper_path': klipper_path, 'python_path': sys.executable}

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
# Script to interact with simulavr by simulating a serial port.
#
# Copyright (C) 2015-2018 Kevin O'Connor <kevin@koconnor.net>
@ -17,7 +17,7 @@ class SerialRxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
pysimulavr.PySimulationMember.__init__(self)
self.terminal = terminal
self.sc = pysimulavr.SystemClock.Instance()
self.delay = SIMULAVR_FREQ / baud
self.delay = SIMULAVR_FREQ // baud
self.current = 0
self.pos = -1
def SetInState(self, pin):
@ -33,7 +33,7 @@ class SerialRxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
if self.pos == 1:
return int(self.delay * 1.5)
if self.pos >= SERIALBITS:
data = chr((self.current >> 1) & 0xff)
data = bytearray([(self.current >> 1) & 0xff])
self.terminal.write(data)
self.pos = -1
self.current = 0
@ -48,10 +48,10 @@ class SerialTxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
self.terminal = terminal
self.SetPin('H')
self.sc = pysimulavr.SystemClock.Instance()
self.delay = SIMULAVR_FREQ / baud
self.delay = SIMULAVR_FREQ // baud
self.current = 0
self.pos = 0
self.queue = ""
self.queue = bytearray()
self.sc.Add(self)
def DoStep(self, trueHwStep):
if not self.pos:
@ -59,9 +59,8 @@ class SerialTxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
data = self.terminal.read()
if not data:
return self.delay * 100
self.queue += data
self.current = (ord(self.queue[0]) << 1) | 0x200
self.queue = self.queue[1:]
self.queue.extend(data)
self.current = (self.queue.pop(0) << 1) | 0x200
newstate = 'L'
if self.current & (1 << self.pos):
newstate = 'H'
@ -109,7 +108,7 @@ class Pacing(pysimulavr.PySimulationMember):
self.next_check_clock = 0
self.rel_time = time.time()
self.best_offset = 0.
self.delay = SIMULAVR_FREQ / 10000
self.delay = SIMULAVR_FREQ // 10000
self.sc.Add(self)
def DoStep(self, trueHwStep):
curtime = time.time()
@ -135,7 +134,7 @@ class TerminalIO:
def read(self):
try:
return os.read(self.fd, 64)
except os.error, e:
except os.error as e:
if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):
pysimulavr.SystemClock.Instance().stop()
return ""
@ -198,7 +197,7 @@ def main():
trace = Tracing(options.tracefile, options.trace)
dev = pysimulavr.AvrFactory.instance().makeDevice(proc)
dev.Load(elffile)
dev.SetClockFreq(SIMULAVR_FREQ / speed)
dev.SetClockFreq(SIMULAVR_FREQ // speed)
sc.Add(dev)
pysimulavr.cvar.sysConHandler.SetUseExit(False)
trace.load_options()

78
scripts/graph_accelerometer.py Executable file
View File

@ -0,0 +1,78 @@
#!/usr/bin/env python2
# Generate adxl345 accelerometer graphs
#
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import optparse
import matplotlib
def parse_log(logname):
f = open(logname, 'r')
out = []
for line in f:
if line.startswith('#'):
continue
parts = line.split(',')
if len(parts) != 4:
continue
try:
fparts = [float(p) for p in parts]
except ValueError:
continue
out.append(fparts)
return out
def plot_accel(data, logname):
half_smooth_samples = 15
first_time = data[0][0]
times = [d[0] - first_time for d in data]
fig, axes = matplotlib.pyplot.subplots(nrows=3, sharex=True)
axes[0].set_title("Accelerometer data (%s)" % (logname,))
axis_names = ['x', 'y', 'z']
for i in range(len(axis_names)):
adata = [d[i+1] for d in data]
avg = sum(adata) / len(adata)
adata = [ad - avg for ad in adata]
ax = axes[i]
ax.plot(times, adata, alpha=0.8)
ax.grid(True)
ax.set_ylabel('%s accel (%+.3f)\n(mm/s^2)' % (axis_names[i], -avg))
axes[-1].set_xlabel('Time (%+.3f)\n(s)' % (-first_time,))
fig.tight_layout()
return fig
def setup_matplotlib(output_to_file):
global matplotlib
if output_to_file:
matplotlib.rcParams.update({'figure.autolayout': True})
matplotlib.use('Agg')
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
import matplotlib.ticker
def main():
# Parse command-line arguments
usage = "%prog [options] <log>"
opts = optparse.OptionParser(usage)
opts.add_option("-o", "--output", type="string", dest="output",
default=None, help="filename of output graph")
options, args = opts.parse_args()
if len(args) != 1:
opts.error("Incorrect number of arguments")
# Parse data
data = parse_log(args[0])
# Draw graph
setup_matplotlib(options.output is not None)
fig = plot_accel(data, args[0])
# Show graph
if options.output is None:
matplotlib.pyplot.show()
else:
fig.set_size_inches(8, 6)
fig.savefig(options.output)
if __name__ == '__main__':
main()

View File

@ -77,6 +77,27 @@ def plot_adc_resolution(config, sensors):
fig.tight_layout()
return fig
def plot_resistance(config, sensors):
# Temperature list
all_temps = [float(i) for i in range(1, 351)]
# Build plot
fig, ax = matplotlib.pyplot.subplots()
pullup = config.getfloat('pullup_resistor', 0.)
ax.set_title("Temperature Sensor (pullup=%.0f)" % (pullup,))
ax.set_ylabel('Resistance (Ohms)')
for sensor in sensors:
sc = config.do_create_sensor(sensor)
adcs = [sc.calc_adc(t) for t in all_temps]
rs = [pullup * adc / (1.0 - adc) for adc in adcs]
ax.plot(all_temps, rs, label=sensor, alpha=0.6)
fontP = matplotlib.font_manager.FontProperties()
fontP.set_size('x-small')
ax.legend(loc='best', prop=fontP)
ax.set_xlabel('Temperature (C)')
ax.grid(True)
fig.tight_layout()
return fig
######################################################################
# Startup
@ -111,6 +132,8 @@ def main():
default=5., help="pullup resistor")
opts.add_option("-s", "--sensors", type="string", dest="sensors",
default="", help="list of sensors (comma separated)")
opts.add_option("-r", "--resistance", action="store_true",
help="graph sensor resistance")
options, args = opts.parse_args()
if len(args) != 0:
opts.error("Incorrect number of arguments")
@ -129,7 +152,10 @@ def main():
# Draw graph
setup_matplotlib(options.output is not None)
fig = plot_adc_resolution(config, sensors)
if options.resistance:
fig = plot_resistance(config, sensors)
else:
fig = plot_adc_resolution(config, sensors)
# Show graph
if options.output is None:

View File

@ -137,6 +137,48 @@ def plot_mcu(data, maxbw):
ax1.grid(True)
return fig
def plot_system(data):
# Generate data for plot
lasttime = data[0]['#sampletime']
lastcputime = float(data[0]['cputime'])
times = []
sysloads = []
cputimes = []
memavails = []
for d in data:
st = d['#sampletime']
timedelta = st - lasttime
if timedelta <= 0.:
continue
lasttime = st
times.append(datetime.datetime.utcfromtimestamp(st))
cputime = float(d['cputime'])
cpudelta = max(0., min(1.5, (cputime - lastcputime) / timedelta))
lastcputime = cputime
cputimes.append(cpudelta * 100.)
sysloads.append(float(d['sysload']) * 100.)
memavails.append(float(d['memavail']))
# Build plot
fig, ax1 = matplotlib.pyplot.subplots()
ax1.set_title("MCU bandwidth and load utilization")
ax1.set_xlabel('Time')
ax1.set_ylabel('Load (% of a core)')
ax1.plot_date(times, sysloads, '-', label='system load',
color='cyan', alpha=0.8)
ax1.plot_date(times, cputimes, '-', label='process time',
color='red', alpha=0.8)
ax2 = ax1.twinx()
ax2.set_ylabel('Available memory (KB)')
ax2.plot_date(times, memavails, '-', label='system memory',
color='yellow', alpha=0.3)
fontP = matplotlib.font_manager.FontProperties()
fontP.set_size('x-small')
ax1.legend(loc='best', prop=fontP)
ax1.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%H:%M'))
ax1.grid(True)
return fig
def plot_frequency(data, mcu):
all_keys = {}
for d in data:
@ -211,6 +253,8 @@ def main():
opts = optparse.OptionParser(usage)
opts.add_option("-f", "--frequency", action="store_true",
help="graph mcu frequency")
opts.add_option("-s", "--system", action="store_true",
help="graph system load")
opts.add_option("-o", "--output", type="string", dest="output",
default=None, help="filename of output graph")
opts.add_option("-t", "--temperature", type="string", dest="heater",
@ -233,6 +277,8 @@ def main():
fig = plot_temperature(data, options.heater)
elif options.frequency:
fig = plot_frequency(data, options.mcu)
elif options.system:
fig = plot_system(data)
else:
fig = plot_mcu(data, MAXBANDWIDTH)

View File

@ -31,7 +31,7 @@ create_virtualenv()
report_status "Updating python virtual environment..."
# Create virtualenv if it doesn't already exist
[ ! -d ${PYTHONDIR} ] && virtualenv ${PYTHONDIR}
[ ! -d ${PYTHONDIR} ] && virtualenv -p python2 ${PYTHONDIR}
# Install/update dependencies
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt

View File

@ -37,7 +37,7 @@ create_virtualenv()
report_status "Updating python virtual environment..."
# Create virtualenv if it doesn't already exist
[ ! -d ${PYTHONDIR} ] && virtualenv ${PYTHONDIR}
[ ! -d ${PYTHONDIR} ] && virtualenv -p python2 ${PYTHONDIR}
# Install/update dependencies
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt

View File

@ -34,7 +34,7 @@ create_virtualenv()
report_status "Updating python virtual environment..."
# Create virtualenv if it doesn't already exist
[ ! -d ${PYTHONDIR} ] && virtualenv ${PYTHONDIR}
[ ! -d ${PYTHONDIR} ] && virtualenv -p python2 ${PYTHONDIR}
# Install/update dependencies
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt

View File

@ -36,7 +36,7 @@ create_virtualenv()
report_status "Updating python virtual environment..."
# Create virtualenv if it doesn't already exist
[ ! -d ${PYTHONDIR} ] && virtualenv ${PYTHONDIR}
[ ! -d ${PYTHONDIR} ] && virtualenv -p python2 ${PYTHONDIR}
# Install/update dependencies
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt

View File

@ -62,5 +62,5 @@ fi
echo -e "\n\n=============== Install python virtualenv\n\n"
cd ${MAIN_DIR}
virtualenv ${BUILD_DIR}/python-env
virtualenv -p python2 ${BUILD_DIR}/python-env
${BUILD_DIR}/python-env/bin/pip install -r ${MAIN_DIR}/scripts/klippy-requirements.txt

134
scripts/update_chitu.py Executable file
View File

@ -0,0 +1,134 @@
#!/usr/bin/env python2
# Encodes STM32 firmwares to be flashable from SD card by Chitu motherboards.
# Relocate firmware to 0x08008800!
# Copied from Marlin and modified.
# Licensed under GPL-3.0
import os
import random
import struct
import uuid
import sys
def calculate_crc(contents, seed):
accumulating_xor_value = seed;
for i in range(0, len(contents), 4):
value = struct.unpack('<I', contents[ i : i + 4])[0]
accumulating_xor_value = accumulating_xor_value ^ value
return accumulating_xor_value
def xor_block(r0, r1, block_number, block_size, file_key):
# This is the loop counter
loop_counter = 0x0
# This is the key length
key_length = 0x18
# This is an initial seed
xor_seed = 0x4bad
# This is the block counter
block_number = xor_seed * block_number
#load the xor key from the file
r7 = file_key
for loop_counter in range(0, block_size):
# meant to make sure different bits of the key are used.
xor_seed = int(loop_counter/key_length)
# IP is a scratch register / R12
ip = loop_counter - (key_length * xor_seed)
# xor_seed = (loop_counter * loop_counter) + block_number
xor_seed = (loop_counter * loop_counter) + block_number
# shift the xor_seed left by the bits in IP.
xor_seed = xor_seed >> ip
# load a byte into IP
ip = r0[loop_counter]
# XOR the seed with r7
xor_seed = xor_seed ^ r7
# and then with IP
xor_seed = xor_seed ^ ip
#Now store the byte back
r1[loop_counter] = xor_seed & 0xFF
#increment the loop_counter
loop_counter = loop_counter + 1
def encode_file(input, output_file, file_length):
input_file = bytearray(input.read())
block_size = 0x800
key_length = 0x18
uid_value = uuid.uuid4()
file_key = int(uid_value.hex[0:8], 16)
xor_crc = 0xef3d4323;
# the input file is exepcted to be in chunks of 0x800
# so round the size
while len(input_file) % block_size != 0:
input_file.extend(b'0x0')
# write the file header
output_file.write(struct.pack(">I", 0x443D2D3F))
# encode the contents using a known file header key
# write the file_key
output_file.write(struct.pack("<I", file_key))
#TODO - how to enforce that the firmware aligns to block boundaries?
block_count = int(len(input_file) / block_size)
print("Block Count is ", block_count)
for block_number in range(0, block_count):
block_offset = (block_number * block_size)
block_end = block_offset + block_size
block_array = bytearray(input_file[block_offset: block_end])
xor_block(block_array, block_array, block_number, block_size, file_key)
for n in range (0, block_size):
input_file[block_offset + n] = block_array[n]
# update the expected CRC value.
xor_crc = calculate_crc(block_array, xor_crc)
# write CRC
output_file.write(struct.pack("<I", xor_crc))
# finally, append the encoded results.
output_file.write(input_file)
return
def main():
if len(sys.argv) != 3:
print("Usage: update_chitu <input_file> <output_file>")
exit(1)
fw, output = sys.argv[1:]
if not os.path.isfile(fw):
print("Usage: update_chitu <input_file> <output_file>")
print("Firmware file", fw, "does not exist")
exit(1)
firmware = open(fw, "rb")
update = open(output, "wb")
length = os.path.getsize(fw)
encode_file(firmware, update, length)
firmware.close()
update.close()
print("Encoding complete.")
if __name__ == '__main__':
main()

View File

@ -7,4 +7,4 @@ src-$(CONFIG_HAVE_GPIO_SPI) += spicmds.c thermocouple.c
src-$(CONFIG_HAVE_GPIO_I2C) += i2ccmds.c
src-$(CONFIG_HAVE_GPIO_HARD_PWM) += pwmcmds.c
src-$(CONFIG_HAVE_GPIO_BITBANGING) += lcd_st7920.c lcd_hd44780.c buttons.c \
tmcuart.c spi_software.c neopixel.c
tmcuart.c spi_software.c neopixel.c sensor_adxl345.c

179
src/sensor_adxl345.c Normal file
View File

@ -0,0 +1,179 @@
// Support for gathering acceleration data from ADXL345 chip
//
// Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
//
// This file may be distributed under the terms of the GNU GPLv3 license.
#include <string.h> // memcpy
#include "board/irq.h" // irq_disable
#include "board/misc.h" // timer_read_time
#include "basecmd.h" // oid_alloc
#include "command.h" // DECL_COMMAND
#include "sched.h" // DECL_TASK
#include "spicmds.h" // spidev_transfer
struct adxl345 {
struct timer timer;
uint32_t rest_ticks;
struct spidev_s *spi;
uint16_t sequence, limit_count;
uint8_t flags, data_count;
uint8_t data[48];
};
enum {
AX_HAVE_START = 1<<0, AX_RUNNING = 1<<1, AX_PENDING = 1<<2,
};
static struct task_wake adxl345_wake;
static uint_fast8_t
adxl345_event(struct timer *timer)
{
struct adxl345 *ax = container_of(timer, struct adxl345, timer);
ax->flags |= AX_PENDING;
sched_wake_task(&adxl345_wake);
return SF_DONE;
}
void
command_config_adxl345(uint32_t *args)
{
struct adxl345 *ax = oid_alloc(args[0], command_config_adxl345
, sizeof(*ax));
ax->timer.func = adxl345_event;
ax->spi = spidev_oid_lookup(args[1]);
}
DECL_COMMAND(command_config_adxl345, "config_adxl345 oid=%c spi_oid=%c");
// Report local measurement buffer
static void
adxl_report(struct adxl345 *ax, uint8_t oid)
{
sendf("adxl345_data oid=%c sequence=%hu data=%*s"
, oid, ax->sequence, ax->data_count, ax->data);
ax->data_count = 0;
ax->sequence++;
}
// Chip registers
#define AR_POWER_CTL 0x2D
#define AR_DATAX0 0x32
#define AR_FIFO_STATUS 0x39
#define AM_READ 0x80
#define AM_MULTI 0x40
// Query accelerometer data
static void
adxl_query(struct adxl345 *ax, uint8_t oid)
{
uint8_t msg[9] = { AR_DATAX0 | AM_READ | AM_MULTI, 0, 0, 0, 0, 0, 0, 0, 0 };
spidev_transfer(ax->spi, 1, sizeof(msg), msg);
memcpy(&ax->data[ax->data_count], &msg[1], 6);
ax->data_count += 6;
if (ax->data_count + 6 > ARRAY_SIZE(ax->data))
adxl_report(ax, oid);
uint_fast8_t fifo_status = msg[8] & ~0x80; // Ignore trigger bit
if (fifo_status >= 31 && ax->limit_count != 0xffff)
ax->limit_count++;
if (fifo_status > 1 && fifo_status <= 32) {
// More data in fifo - wake this task again
sched_wake_task(&adxl345_wake);
} else if (ax->flags & AX_RUNNING) {
// Sleep until next check time
sched_del_timer(&ax->timer);
ax->flags &= ~AX_PENDING;
irq_disable();
ax->timer.waketime = timer_read_time() + ax->rest_ticks;
sched_add_timer(&ax->timer);
irq_enable();
}
}
// Startup measurements
static void
adxl_start(struct adxl345 *ax, uint8_t oid)
{
sched_del_timer(&ax->timer);
ax->flags = AX_RUNNING;
uint8_t msg[2] = { AR_POWER_CTL, 0x08 };
uint32_t start1_time = timer_read_time();
spidev_transfer(ax->spi, 0, sizeof(msg), msg);
irq_disable();
uint32_t start2_time = timer_read_time();
ax->timer.waketime = start2_time + ax->rest_ticks;
sched_add_timer(&ax->timer);
irq_enable();
sendf("adxl345_start oid=%c start1_time=%u start2_time=%u"
, oid, start1_time, start2_time);
}
// End measurements
static void
adxl_stop(struct adxl345 *ax, uint8_t oid)
{
// Disable measurements
sched_del_timer(&ax->timer);
ax->flags = 0;
uint8_t msg[2] = { AR_POWER_CTL, 0x00 };
uint32_t end1_time = timer_read_time();
spidev_transfer(ax->spi, 0, sizeof(msg), msg);
uint32_t end2_time = timer_read_time();
// Drain any measurements still in fifo
uint_fast8_t i;
for (i=0; i<33; i++) {
msg[0] = AR_FIFO_STATUS | AM_READ;
msg[1] = 0;
spidev_transfer(ax->spi, 1, sizeof(msg), msg);
if (!(msg[1] & 0x3f))
break;
adxl_query(ax, oid);
}
// Report final data
if (ax->data_count)
adxl_report(ax, oid);
sendf("adxl345_end oid=%c end1_time=%u end2_time=%u"
" limit_count=%hu sequence=%hu"
, oid, end1_time, end2_time, ax->limit_count, ax->sequence);
}
void
command_query_adxl345(uint32_t *args)
{
struct adxl345 *ax = oid_lookup(args[0], command_config_adxl345);
if (!args[2]) {
// End measurements
adxl_stop(ax, args[0]);
return;
}
// Start new measurements query
sched_del_timer(&ax->timer);
ax->timer.waketime = args[1];
ax->rest_ticks = args[2];
ax->flags = AX_HAVE_START;
ax->sequence = ax->limit_count = 0;
ax->data_count = 0;
sched_add_timer(&ax->timer);
}
DECL_COMMAND(command_query_adxl345,
"query_adxl345 oid=%c clock=%u rest_ticks=%u");
void
adxl345_task(void)
{
if (!sched_check_wake(&adxl345_wake))
return;
uint8_t oid;
struct adxl345 *ax;
foreach_oid(oid, ax, command_config_adxl345) {
uint_fast8_t flags = ax->flags;
if (!(flags & AX_PENDING))
continue;
if (flags & AX_HAVE_START)
adxl_start(ax, oid);
else
adxl_query(ax, oid);
}
}
DECL_TASK(adxl345_task);

View File

@ -116,6 +116,8 @@ choice
bool "28KiB bootloader" if MACH_STM32F103
config STM32_FLASH_START_8000
bool "32KiB bootloader (SKR-PRO or TFT35-V3.0)" if MACH_STM32F207 || MACH_STM32F407
config STM32_FLASH_START_8800
bool "34KiB bootloader (Chitu v6 Bootloader)" if MACH_STM32F103
config STM32_FLASH_START_10000
bool "64KiB bootloader (Alfawise)" if MACH_STM32F103
config STM32_FLASH_START_0000
@ -129,6 +131,7 @@ config FLASH_START
default 0x8005000 if STM32_FLASH_START_5000
default 0x8007000 if STM32_FLASH_START_7000
default 0x8008000 if STM32_FLASH_START_8000
default 0x8008800 if STM32_FLASH_START_8800
default 0x8010000 if STM32_FLASH_START_10000
default 0x8000000

View File

@ -127,6 +127,8 @@ CONFIG ../../config/generic-mks-robin-e3.cfg
CONFIG ../../config/generic-mks-robin-nano.cfg
CONFIG ../../config/printer-alfawise-u30-2018.cfg
CONFIG ../../config/printer-creality-ender3-v2-2020.cfg
CONFIG ../../config/printer-creality-ender3pro-2020.cfg
CONFIG ../../config/printer-tronxy-x5sa-v6-2019.cfg
CONFIG ../../config/printer-twotrees-sapphire-pro-2020.cfg
# Printers using the stm32f407