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. # measurements, e.g. with an accelerometer.
# Default value is 0.1 which is a good all-round value for most printers. # 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 # Config file helpers
@ -1304,6 +1330,14 @@
#pin: #pin:
# The pin on which the button is connected. This parameter must be # The pin on which the button is connected. This parameter must be
# provided. # 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: #press_gcode:
# A list of G-Code commands to execute when the button is pressed. # A list of G-Code commands to execute when the button is pressed.
# G-Code templates are supported. This parameter must be provided. # 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 # Set the Vcomh value on SSD1306/SH1106 displays. This value is
# associated with a "smearing" effect on some OLED displays. # associated with a "smearing" effect on some OLED displays.
# The value may range from 0 to 63. Default is 0. # 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 #invert: FALSE
# TRUE inverts the pixels on certain OLED (SSD1306/SH1106) displays # TRUE inverts the pixels on certain OLED (SSD1306/SH1106) displays
# The default is FALSE # The default is FALSE
@ -2188,6 +2224,23 @@
# communication. The pin must have a valid pinmux configuration # communication. The pin must have a valid pinmux configuration
# for the given SERCOM peripheral. This parameter must be provided. # 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 # Replicape support - see the generic-replicape.cfg file for further
# details. # details.
#[replicape] #[replicape]

View File

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

View File

@ -134,3 +134,31 @@ pin: replicape:power_fan0
# PWM output pin controlling the servo. This parameter must be # PWM output pin controlling the servo. This parameter must be
# provided. # 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 ### 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_ _Default Method: automatic if a probe is detected, otherwise manual_
Initiates the probing procedure for Bed Mesh Calibration. If `METHOD=manual` Initiates the probing procedure for Bed Mesh Calibration. If `METHOD=manual`
is selected then manual probing will occur. When switching between automatic is selected then manual probing will occur. When switching between automatic
and manual probing the generated mesh points will automatically be adjusted. 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 ### Profiles
`BED_MESH_PROFILE SAVE=name LOAD=name REMOVE=name` `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 "Tool Adjusted" points refer to the nozzle location for each point, and
the "Probe" points refer to the probe location. Note that when manually 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. 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 - `printer.query_endstops.last_query["<endstop>"]`: Returns True if
the given endstop was reported as "triggered" during the last the given endstop was reported as "triggered" during the last
QUERY_ENDSTOP command. Note, due to the order of template expansion QUERY_ENDSTOP command. Note, due to the order of template expansion
(see above), the QUERY_STATUS command must be run prior to the macro (see above), the QUERY_ENDSTOP command must be run prior to the
containing this reference. 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 - `printer.configfile.config["<section>"]["<option>"]`: Returns the
given config file setting as read by Klipper during the last given config file setting as read by Klipper during the last
software start or restart. (Any settings changed at run-time will 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 # 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 20200816: The gcode macro `printer.gcode` object has been renamed to
`printer.gcode_move`. Several undocumented variables in `printer.gcode_move`. Several undocumented variables in
`printer.toolhead` and `printer.gcode` have been removed. See `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) [install-octopi.sh](https://github.com/KevinOConnor/klipper/tree/master/scripts/install-octopi.sh)
script for further information on the necessary Linux admin steps. 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 Note: If you are not using an OctoPi image, be aware that several
Linux distributions enable a "ModemManager" (or similar) package that Linux distributions enable a "ModemManager" (or similar) package that
can disrupt serial communication. (Which can cause Klipper to report 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 The following commands are available when the "bed_mesh" config
section is enabled: 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 This command probes the bed using generated points specified by the
parameters in the config. After probing, a mesh is generated and parameters in the config. After probing, a mesh is generated and
z-movement is adjusted according to the mesh. See the PROBE command 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 the given distance (in mm) at the given constant velocity (in
mm/s). If ACCEL is specified and is greater than zero, then the mm/s). If ACCEL is specified and is greater than zero, then the
given acceleration (in mm/s^2) will be used; otherwise no given acceleration (in mm/s^2) will be used; otherwise no
acceleration is performed. No boundary checks are performed; no acceleration is performed. If acceleration is not performed then it
kinematic updates are made; other parallel steppers on an axis will can lead to the micro-controller reporting "No next step" errors
not be moved. Use caution as an incorrect command could cause (avoid these errors by specifying an ACCEL value or use a very low
damage! Using this command will almost certainly place the low-level VELOCITY). No boundary checks are performed; no kinematic updates
kinematics in an incorrect state; issue a G28 afterwards to reset are made; other parallel steppers on an axis will not be moved. Use
the kinematics. This command is intended for low-level diagnostics caution as an incorrect command could cause damage! Using this
and debugging. 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 - `SET_KINEMATIC_POSITION [X=<value>] [Y=<value>] [Z=<value>]`: Force
the low-level kinematic code to believe the toolhead is at the given the low-level kinematic code to believe the toolhead is at the given
cartesian position. This is a diagnostic and debugging command; use 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 [target=<target_temperature>]`: Sets the target temperature for a
temperature_fan. If a target is not supplied, it is set to the temperature_fan. If a target is not supplied, it is set to the
specified temperature in the config file. 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 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 [octopi releases](https://github.com/guysoft/OctoPi/releases) for
release information. One should verify that OctoPi boots and that the release information. One should verify that OctoPi boots and that the
OctoPrint web server works. After connecting to the OctoPrint web 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 After installing OctoPi and upgrading OctoPrint, it will be necessary
to ssh into the target machine to run a handful of system commands. If 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 # 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. # This file may be distributed under the terms of the GNU GPLv3 license.
import os, logging import os, logging
@ -11,9 +11,11 @@ import cffi
# c_helper.so compiling # c_helper.so compiling
###################################################################### ######################################################################
COMPILE_CMD = ("gcc -Wall -g -O2 -shared -fPIC" GCC_CMD = "gcc"
" -flto -fwhole-program -fno-use-linker-plugin" COMPILE_ARGS = ("-Wall -g -O2 -shared -fPIC"
" -o %s %s") " -flto -fwhole-program -fno-use-linker-plugin"
" -o %s %s")
SSE_FLAGS = "-mfpmath=sse -msse2"
SOURCE_FILES = [ SOURCE_FILES = [
'pyhelper.c', 'serialqueue.c', 'stepcompress.c', 'itersolve.c', 'trapq.c', 'pyhelper.c', 'serialqueue.c', 'stepcompress.c', 'itersolve.c', 'trapq.c',
'kin_cartesian.c', 'kin_corexy.c', 'kin_corexz.c', 'kin_delta.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 # Check if the code needs to be compiled
def check_build_code(srcdir, target, sources, cmd, other_files=[]): 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]) obj_times = get_mtimes(srcdir, [target])
if not obj_times or max(src_times) > min(obj_times): if not obj_times or max(src_times) > min(obj_times):
logging.info("Building C code module %s", target) 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) logging.error(msg)
raise Exception(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_main = None
FFI_lib = None FFI_lib = None
pyhelper_logging_callback = None pyhelper_logging_callback = None
@ -211,8 +219,11 @@ def get_ffi():
global FFI_main, FFI_lib, pyhelper_logging_callback global FFI_main, FFI_lib, pyhelper_logging_callback
if FFI_lib is None: if FFI_lib is None:
srcdir = os.path.dirname(os.path.realpath(__file__)) srcdir = os.path.dirname(os.path.realpath(__file__))
check_build_code(srcdir, DEST_LIB, SOURCE_FILES, COMPILE_CMD if check_gcc_option(SSE_FLAGS):
, OTHER_FILES) 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() FFI_main = cffi.FFI()
for d in defs_all: for d in defs_all:
FFI_main.cdef(d) FFI_main.cdef(d)

View File

@ -13,6 +13,225 @@
#include "itersolve.h" // struct stepper_kinematics #include "itersolve.h" // struct stepper_kinematics
#include "trapq.h" // struct move #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 * 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); 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 // Calculate the position from the convolution of the shaper with input signal
static inline double static inline double
calc_position(struct move *m, int axis, double move_time calc_position(struct move *m, int axis, double move_time
, struct shaper_pulse *pulses, int n) , struct shaper_pulses *sp)
{ {
double res = 0.; double res = 0.;
int i; int num_pulses = sp->num_pulses, i;
for (i = 0; i < n; ++i) for (i = 0; i < num_pulses; ++i) {
res += pulses[i].a * get_axis_position_across_moves( double t = sp->pulses[i].t, a = sp->pulses[i].a;
m, axis, move_time + pulses[i].t); res += a * get_axis_position_across_moves(m, axis, move_time + t);
}
return res; 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 * Kinematics-related shaper code
@ -265,15 +280,22 @@ shift_pulses(int n, struct shaper_pulse *pulses)
#define DUMMY_T 500.0 #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 // Optimized calc_position when only x axis is needed
static double static double
shaper_x_calc_position(struct stepper_kinematics *sk, struct move *m shaper_x_calc_position(struct stepper_kinematics *sk, struct move *m
, double move_time) , double move_time)
{ {
struct input_shaper *is = container_of(sk, struct input_shaper, sk); 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); 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); 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) , double move_time)
{ {
struct input_shaper *is = container_of(sk, struct input_shaper, sk); 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); 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); 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) , double move_time)
{ {
struct input_shaper *is = container_of(sk, struct input_shaper, sk); 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); return is->orig_sk->calc_position_cb(is->orig_sk, m, move_time);
is->m.start_pos = move_get_coord(m, move_time); is->m.start_pos = move_get_coord(m, move_time);
if (is->x_n) if (is->sx.num_pulses)
is->m.start_pos.x = calc_position(m, 'x', move_time is->m.start_pos.x = calc_position(m, 'x', move_time, &is->sx);
, is->x_pulses, is->x_n); if (is->sy.num_pulses)
if (is->y_n) is->m.start_pos.y = calc_position(m, 'y', move_time, &is->sy);
is->m.start_pos.y = calc_position(m, 'y', move_time
, is->y_pulses, is->y_n);
return is->orig_sk->calc_position_cb(is->orig_sk, &is->m, DUMMY_T); 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 int __visible
input_shaper_set_sk(struct stepper_kinematics *sk input_shaper_set_sk(struct stepper_kinematics *sk
, struct stepper_kinematics *orig_sk) , struct stepper_kinematics *orig_sk)
{ {
struct input_shaper *is = container_of(sk, struct input_shaper, sk); struct input_shaper *is = container_of(sk, struct input_shaper, sk);
int af = orig_sk->active_flags & (AF_X | AF_Y); if (orig_sk->active_flags == AF_X)
if (af == (AF_X | AF_Y))
is->sk.calc_position_cb = shaper_xy_calc_position;
else if (af & AF_X)
is->sk.calc_position_cb = shaper_x_calc_position; 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; 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 else
return -1; return -1;
is->sk.active_flags = orig_sk->active_flags; is->sk.active_flags = orig_sk->active_flags;
@ -344,14 +345,23 @@ input_shaper_set_sk(struct stepper_kinematics *sk
return 0; return 0;
} }
static is_init_shaper_callback init_shaper_callbacks[] = { static void
[INPUT_SHAPER_ZV] = &init_shaper_zv, shaper_note_generation_time(struct input_shaper *is)
[INPUT_SHAPER_ZVD] = &init_shaper_zvd, {
[INPUT_SHAPER_MZV] = &init_shaper_mzv, double pre_active = 0., post_active = 0.;
[INPUT_SHAPER_EI] = &init_shaper_ei, if ((is->sk.active_flags & AF_X) && is->sx.num_pulses) {
[INPUT_SHAPER_2HUMP_EI] = &init_shaper_2hump_ei, pre_active = is->sx.pulses[is->sx.num_pulses-1].t;
[INPUT_SHAPER_3HUMP_EI] = &init_shaper_3hump_ei, 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 int __visible
input_shaper_set_shaper_params(struct stepper_kinematics *sk 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) , double damping_ratio_y)
{ {
struct input_shaper *is = container_of(sk, struct input_shaper, sk); struct input_shaper *is = container_of(sk, struct input_shaper, sk);
if (is->orig_sk->active_flags & AF_X)
if (shaper_type_x >= ARRAY_SIZE(init_shaper_callbacks) || shaper_type_x < 0) init_shaper(shaper_type_x, shaper_freq_x, damping_ratio_x, &is->sx);
return -1; else
if (shaper_type_y >= ARRAY_SIZE(init_shaper_callbacks) || shaper_type_y < 0) is->sx.num_pulses = 0;
return -1; if (is->orig_sk->active_flags & AF_Y)
init_shaper(shaper_type_y, shaper_freq_y, damping_ratio_y, &is->sy);
int af = is->orig_sk->active_flags & (AF_X | AF_Y); else
free(is->x_pulses); is->sy.num_pulses = 0;
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;
}
shaper_note_generation_time(is); shaper_note_generation_time(is);
return 0; return 0;
} }
@ -396,19 +389,13 @@ double __visible
input_shaper_get_step_generation_window(int shaper_type, double shaper_freq input_shaper_get_step_generation_window(int shaper_type, double shaper_freq
, double damping_ratio) , 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.; return 0.;
if (shaper_type >= ARRAY_SIZE(init_shaper_callbacks) || shaper_type < 0) double window = -sp.pulses[0].t;
return 0.; if (sp.pulses[sp.num_pulses-1].t > window)
is_init_shaper_callback init_shaper_cb = init_shaper_callbacks[shaper_type]; window = sp.pulses[sp.num_pulses-1].t;
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);
return window; 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.): 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) 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)] 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'] ALGOS = ['lagrange', 'bicubic']
def __init__(self, config, bedmesh): def __init__(self, config, bedmesh):
self.printer = config.get_printer() self.printer = config.get_printer()
self.orig_config = {'radius': None, 'origin': None}
self.radius = self.origin = None self.radius = self.origin = None
self.mesh_min = self.mesh_max = (0., 0.)
self.relative_reference_index = config.getint( self.relative_reference_index = config.getint(
'relative_reference_index', None) 'relative_reference_index', None)
self.orig_config['rri'] = self.relative_reference_index
self.bedmesh = bedmesh self.bedmesh = bedmesh
self.mesh_config = collections.OrderedDict() self.mesh_config = collections.OrderedDict()
self.points = self._generate_points(config) self._init_mesh_config(config)
self._init_mesh_config(config, self.points) self._generate_points(config.error)
self.orig_points = self.points
self.probe_helper = probe.ProbePointsHelper( self.probe_helper = probe.ProbePointsHelper(
config, self.probe_finalize, self.points) config, self.probe_finalize, self.points)
self.probe_helper.minimum_points(3) self.probe_helper.minimum_points(3)
@ -250,37 +254,18 @@ class BedMeshCalibrate:
self.gcode.register_command( self.gcode.register_command(
'BED_MESH_CALIBRATE', self.cmd_BED_MESH_CALIBRATE, 'BED_MESH_CALIBRATE', self.cmd_BED_MESH_CALIBRATE,
desc=self.cmd_BED_MESH_CALIBRATE_help) desc=self.cmd_BED_MESH_CALIBRATE_help)
def _generate_points(self, config): def _generate_points(self, error):
self.radius = config.getfloat('mesh_radius', None, above=0.) x_cnt = self.mesh_config['x_count']
if self.radius is not None: y_cnt = self.mesh_config['y_count']
self.origin = parse_pair(config, ('mesh_origin', "0, 0")) min_x, min_y = self.mesh_min
x_cnt = y_cnt = config.getint('round_probe_count', 5, minval=3) max_x, max_y = self.mesh_max
# 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
x_dist = (max_x - min_x) / (x_cnt - 1) x_dist = (max_x - min_x) / (x_cnt - 1)
y_dist = (max_y - min_y) / (y_cnt - 1) y_dist = (max_y - min_y) / (y_cnt - 1)
# floor distances down to next hundredth # floor distances down to next hundredth
x_dist = math.floor(x_dist * 100) / 100 x_dist = math.floor(x_dist * 100) / 100
y_dist = math.floor(y_dist * 100) / 100 y_dist = math.floor(y_dist * 100) / 100
if x_dist <= 1. or y_dist <= 1.: if x_dist <= 1. or y_dist <= 1.:
raise 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: if self.radius is not None:
# round bed, min/max needs to be recalculated # round bed, min/max needs to be recalculated
@ -311,7 +296,7 @@ class BedMeshCalibrate:
points.append( points.append(
(self.origin[0] + pos_x, self.origin[1] + pos_y)) (self.origin[0] + pos_x, self.origin[1] + pos_y))
pos_y += y_dist pos_y += y_dist
return points self.points = points
def print_generated_points(self, print_func): def print_generated_points(self, print_func):
x_offset = y_offset = 0. x_offset = y_offset = 0.
probe = self.printer.lookup_object('probe', None) probe = self.printer.lookup_object('probe', None)
@ -329,33 +314,69 @@ class BedMeshCalibrate:
print_func( print_func(
"bed_mesh: relative_reference_index %d is (%.2f, %.2f)" "bed_mesh: relative_reference_index %d is (%.2f, %.2f)"
% (rri, self.points[rri][0], self.points[rri][1])) % (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, pps = parse_pair(config, ('mesh_pps', '2'), check=False,
cast=int, minval=0) 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 = self.mesh_config
params['mesh_x_pps'] = pps[0] x_pps = params['mesh_x_pps']
params['mesh_y_pps'] = pps[1] y_pps = params['mesh_y_pps']
params['algo'] = config.get('algorithm', 'lagrange').strip().lower()
if params['algo'] not in self.ALGOS: if params['algo'] not in self.ALGOS:
raise config.error( raise error(
"bed_mesh: Unknown algorithm <%s>" "bed_mesh: Unknown algorithm <%s>"
% (self.mesh_config['algo'])) % (self.mesh_config['algo']))
# Check the algorithm against the current configuration # Check the algorithm against the current configuration
max_probe_cnt = max(params['x_count'], params['y_count']) max_probe_cnt = max(params['x_count'], params['y_count'])
min_probe_cnt = min(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 # Interpolation disabled
self.mesh_config['algo'] = 'direct' self.mesh_config['algo'] = 'direct'
elif params['algo'] == 'lagrange' and max_probe_cnt > 6: elif params['algo'] == 'lagrange' and max_probe_cnt > 6:
# Lagrange interpolation tends to oscillate when using more # Lagrange interpolation tends to oscillate when using more
# than 6 samples # than 6 samples
raise config.error( raise error(
"bed_mesh: cannot exceed a probe_count of 6 when using " "bed_mesh: cannot exceed a probe_count of 6 when using "
"lagrange interpolation. Configured Probe Count: %d, %d" % "lagrange interpolation. Configured Probe Count: %d, %d" %
(self.mesh_config['x_count'], self.mesh_config['y_count'])) (self.mesh_config['x_count'], self.mesh_config['y_count']))
elif params['algo'] == 'bicubic' and min_probe_cnt < 4: elif params['algo'] == 'bicubic' and min_probe_cnt < 4:
if max_probe_cnt > 6: if max_probe_cnt > 6:
raise config.error( raise error(
"bed_mesh: invalid probe_count option when using bicubic " "bed_mesh: invalid probe_count option when using bicubic "
"interpolation. Combination of 3 points on one axis with " "interpolation. Combination of 3 points on one axis with "
"more than 6 on another is not permitted. " "more than 6 on another is not permitted. "
@ -368,11 +389,74 @@ class BedMeshCalibrate:
"interpolation. Configured Probe Count: %d, %d" % "interpolation. Configured Probe Count: %d, %d" %
(self.mesh_config['x_count'], self.mesh_config['y_count'])) (self.mesh_config['x_count'], self.mesh_config['y_count']))
params['algo'] = 'lagrange' params['algo'] = 'lagrange'
params['tension'] = config.getfloat( def update_config(self, gcmd):
'bicubic_tension', .2, minval=0., maxval=2.) # 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" cmd_BED_MESH_CALIBRATE_help = "Perform Mesh Bed Leveling"
def cmd_BED_MESH_CALIBRATE(self, gcmd): def cmd_BED_MESH_CALIBRATE(self, gcmd):
self.bedmesh.set_mesh(None) self.bedmesh.set_mesh(None)
self.update_config(gcmd)
self.probe_helper.start_probe(gcmd) self.probe_helper.start_probe(gcmd)
def probe_finalize(self, offsets, positions): def probe_finalize(self, offsets, positions):
x_offset, y_offset, z_offset = offsets x_offset, y_offset, z_offset = offsets

View File

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

View File

@ -62,7 +62,7 @@ class MCU_buttons:
ack_diff -= 0x100 ack_diff -= 0x100
msg_ack_count = ack_count - ack_diff msg_ack_count = ack_count - ack_diff
# Determine new buttons # Determine new buttons
buttons = params['state'] buttons = bytearray(params['state'])
new_count = msg_ack_count + len(buttons) - self.ack_count new_count = msg_ack_count + len(buttons) - self.ack_count
if new_count <= 0: if new_count <= 0:
return return
@ -71,9 +71,9 @@ class MCU_buttons:
self.ack_cmd.send([self.oid, new_count]) self.ack_cmd.send([self.oid, new_count])
self.ack_count += new_count self.ack_count += new_count
# Call self.handle_button() with this event in main thread # 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( 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): def handle_button(self, eventtime, button):
button ^= self.invert button ^= self.invert
changed = button ^ self.last_button changed = button ^ self.last_button

View File

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

View File

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

View File

@ -103,7 +103,7 @@ class HD44780:
data = self.icons.get(glyph_name) data = self.icons.get(glyph_name)
if data is not None: if data is not None:
slot, bits = data 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 self.glyph_framebuffer[slot * 8:(slot + 1) * 8] = bits
return 1 return 1
char = TextGlyphs.get(glyph_name) char = TextGlyphs.get(glyph_name)

View File

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

View File

@ -13,10 +13,11 @@ BACKGROUND_PRIORITY_CLOCK = 0x7fffffff00000000
TextGlyphs = { 'right_arrow': '\x1a', 'degrees': '\xf8' } TextGlyphs = { 'right_arrow': '\x1a', 'degrees': '\xf8' }
class DisplayBase: class DisplayBase:
def __init__(self, io, columns=128): def __init__(self, io, columns=128, x_offset=0):
self.send = io.send self.send = io.send
# framebuffers # framebuffers
self.columns = columns self.columns = columns
self.x_offset = x_offset
self.vram = [bytearray(self.columns) for i in range(8)] self.vram = [bytearray(self.columns) for i in range(8)]
self.all_framebuffers = [(self.vram[i], bytearray('~'*self.columns), i) self.all_framebuffers = [(self.vram[i], bytearray('~'*self.columns), i)
for i in range(8)] for i in range(8)]
@ -71,10 +72,11 @@ class DisplayBase:
if x + len(data) > 16: if x + len(data) > 16:
data = data[:16 - min(x, 16)] data = data[:16 - min(x, 16)]
pix_x = x * 8 pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2] page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1] page_bot = self.vram[y * 2 + 1]
for c in data: for c in bytearray(data):
bits_top, bits_bot = self.font[ord(c)] bits_top, bits_bot = self.font[c]
page_top[pix_x:pix_x+8] = bits_top page_top[pix_x:pix_x+8] = bits_top
page_bot[pix_x:pix_x+8] = bits_bot page_bot[pix_x:pix_x+8] = bits_bot
pix_x += 8 pix_x += 8
@ -83,6 +85,7 @@ class DisplayBase:
return return
bits_top, bits_bot = self._swizzle_bits(data) bits_top, bits_bot = self._swizzle_bits(data)
pix_x = x * 8 pix_x = x * 8
pix_x += self.x_offset
page_top = self.vram[y * 2] page_top = self.vram[y * 2]
page_bot = self.vram[y * 2 + 1] page_bot = self.vram[y * 2 + 1]
for i in range(8): for i in range(8):
@ -93,6 +96,7 @@ class DisplayBase:
if icon is not None and x < 15: if icon is not None and x < 15:
# Draw icon in graphics mode # Draw icon in graphics mode
pix_x = x * 8 pix_x = x * 8
pix_x += self.x_offset
page_idx = y * 2 page_idx = y * 2
self.vram[page_idx][pix_x:pix_x+16] = icon[0] self.vram[page_idx][pix_x:pix_x+16] = icon[0]
self.vram[page_idx + 1][pix_x:pix_x+16] = icon[1] 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 # The SSD1306 supports both i2c and "4-wire" spi
class SSD1306(DisplayBase): 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) cs_pin = config.get("cs_pin", None)
if cs_pin is None: if cs_pin is None:
io = I2C(config, 60) io = I2C(config, 60)
@ -201,7 +205,7 @@ class SSD1306(DisplayBase):
io = SPI4wire(config, "dc_pin") io = SPI4wire(config, "dc_pin")
io_bus = io.spi io_bus = io.spi
self.reset = ResetHelper(config.get("reset_pin", None), io_bus) 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.contrast = config.getint('contrast', 239, minval=0, maxval=255)
self.vcomh = config.getint('vcomh', 0, minval=0, maxval=63) self.vcomh = config.getint('vcomh', 0, minval=0, maxval=63)
self.invert = config.getboolean('invert', False) self.invert = config.getboolean('invert', False)
@ -232,4 +236,5 @@ class SSD1306(DisplayBase):
# the SH1106 is SSD1306 compatible with up to 132 columns # the SH1106 is SSD1306 compatible with up to 132 columns
class SH1106(SSD1306): class SH1106(SSD1306):
def __init__(self, config): 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. # This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging import math, logging
import homing
TRINAMIC_DRIVERS = ["tmc2130", "tmc2208", "tmc2209", "tmc2660", "tmc5160"] TRINAMIC_DRIVERS = ["tmc2130", "tmc2208", "tmc2209", "tmc2660", "tmc5160"]
@ -81,7 +80,7 @@ class EndstopPhase:
except Exception as e: except Exception as e:
msg = "Unable to get stepper %s phase: %s" % (self.name, str(e)) msg = "Unable to get stepper %s phase: %s" % (self.name, str(e))
logging.exception(msg) logging.exception(msg)
raise homing.EndstopError(msg) raise self.printer.command_error(msg)
if stepper.is_dir_inverted(): if stepper.is_dir_inverted():
phase = (self.phases - 1) - phase phase = (self.phases - 1) - phase
else: else:
@ -95,7 +94,7 @@ class EndstopPhase:
if delta >= self.phases - self.endstop_phase_accuracy: if delta >= self.phases - self.endstop_phase_accuracy:
delta -= self.phases delta -= self.phases
elif delta > self.endstop_phase_accuracy: elif delta > self.endstop_phase_accuracy:
raise homing.EndstopError( raise self.printer.command_error(
"Endstop %s incorrect phase (got %d vs %d)" % ( "Endstop %s incorrect phase (got %d vs %d)" % (
self.name, phase, self.endstop_phase)) self.name, phase, self.endstop_phase))
return delta * self.step_dist return delta * self.step_dist

View File

@ -12,7 +12,17 @@ class GCodeButton:
self.pin = config.get('pin') self.pin = config.get('pin')
self.last_state = 0 self.last_state = 0
buttons = self.printer.load_object(config, "buttons") 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') gcode_macro = self.printer.load_object(config, 'gcode_macro')
self.press_template = gcode_macro.load_template(config, 'press_gcode') self.press_template = gcode_macro.load_template(config, 'press_gcode')
self.release_template = gcode_macro.load_template(config, self.release_template = gcode_macro.load_template(config,

View File

@ -74,10 +74,10 @@ class Heater:
pwm_time = read_time + self.pwm_delay pwm_time = read_time + self.pwm_delay
self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME
self.last_pwm_value = value 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) 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): def temperature_callback(self, read_time, temp):
with self.lock: with self.lock:
time_diff = read_time - self.last_temp_time time_diff = read_time - self.last_temp_time

View File

@ -124,7 +124,7 @@ class InputShaper:
self.shapers.keys()[ self.shapers.keys()[
self.shapers.values().index(shaper_type_x)] self.shapers.values().index(shaper_type_x)]
, self.shapers.keys()[ , self.shapers.keys()[
self.shapers.values().index(shaper_type_x)] self.shapers.values().index(shaper_type_y)]
, shaper_freq_x, shaper_freq_y , shaper_freq_x, shaper_freq_y
, damping_ratio_x, damping_ratio_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. # This file may be distributed under the terms of the GNU GPLv3 license.
import logging, bisect import logging, bisect
import homing
class ManualProbe: class ManualProbe:
def __init__(self, config): def __init__(self, config):
@ -97,7 +96,7 @@ class ManualProbeHelper:
if curpos[2] < z_bob_pos: if curpos[2] < z_bob_pos:
self.toolhead.manual_move([None, None, z_bob_pos], self.speed) self.toolhead.manual_move([None, None, z_bob_pos], self.speed)
self.toolhead.manual_move([None, None, z_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) self.finalize(False)
raise raise
def report_z_status(self, warn_no_change=False, prev_pos=None): 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> # Copyright (C) 2019 Kevin O'Connor <kevin@koconnor.net>
# #
# This file may be distributed under the terms of the GNU GPLv3 license. # This file may be distributed under the terms of the GNU GPLv3 license.
import stepper, homing, chelper import stepper, chelper
from . import force_move from . import force_move
ENDSTOP_SAMPLE_TIME = .000015 ENDSTOP_SAMPLE_TIME = .000015
@ -110,7 +110,7 @@ class ManualStepper:
error = str(e) error = str(e)
self.sync_print_time() self.sync_print_time()
if error is not None: if error is not None:
raise homing.CommandError(error) raise self.printer.command_error(error)
cmd_MANUAL_STEPPER_help = "Command a manually configured stepper" cmd_MANUAL_STEPPER_help = "Command a manually configured stepper"
def cmd_MANUAL_STEPPER(self, gcmd): def cmd_MANUAL_STEPPER(self, gcmd):
enable = gcmd.get_int('ENABLE', None) enable = gcmd.get_int('ENABLE', None)

View File

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

View File

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

View File

@ -6,6 +6,22 @@
import logging import logging
from . import probe, z_tilt 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: class QuadGantryLevel:
def __init__(self, config): def __init__(self, config):
self.printer = config.get_printer() self.printer = config.get_printer()
@ -48,29 +64,36 @@ class QuadGantryLevel:
" ".join(["%s: %.6f" % (z_id, z_positions[z_id]) " ".join(["%s: %.6f" % (z_id, z_positions[z_id])
for z_id in range(len(z_positions))])) for z_id in range(len(z_positions))]))
self.gcode.respond_info(points_message) self.gcode.respond_info(points_message)
p1 = [positions[0][0] + offsets[0],z_positions[0]] # Calculate slope along X axis between probe point 0 and 3
p2 = [positions[1][0] + offsets[0],z_positions[1]] ppx0 = [positions[0][0] + offsets[0], z_positions[0]]
p3 = [positions[2][0] + offsets[0],z_positions[2]] ppx3 = [positions[3][0] + offsets[0], z_positions[3]]
p4 = [positions[3][0] + offsets[0],z_positions[3]] slope_x_pp03 = self.linefit(ppx0, ppx3)
f1 = self.linefit(p1,p4) # Calculate slope along X axis between probe point 1 and 2
f2 = self.linefit(p2,p3) ppx1 = [positions[1][0] + offsets[0], z_positions[1]]
logging.info("quad_gantry_level f1: %s, f2: %s" % (f1,f2)) 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], 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], 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], 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], b2 = [positions[1][1] + offsets[1],
self.plot(f2,self.gantry_corners[1][0])] self.plot(slope_x_pp12, self.gantry_corners[1][0])]
af = self.linefit(a1,a2) slope_y_s23 = self.linefit(b1, b2)
bf = self.linefit(b1,b2) logging.info("quad_gantry_level af: %s, bf: %s"
logging.info("quad_gantry_level af: %s, bf: %s" % (af,bf)) % (slope_y_s01, slope_y_s23))
# Calculate z height of each stepper
z_height = [0,0,0,0] z_height = [0,0,0,0]
z_height[0] = self.plot(af,self.gantry_corners[0][1]) z_height[0] = self.plot(slope_y_s01, self.gantry_corners[0][1])
z_height[1] = self.plot(af,self.gantry_corners[1][1]) z_height[1] = self.plot(slope_y_s01, self.gantry_corners[1][1])
z_height[2] = self.plot(bf,self.gantry_corners[1][1]) z_height[2] = self.plot(slope_y_s23, self.gantry_corners[1][1])
z_height[3] = self.plot(bf,self.gantry_corners[0][1]) z_height[3] = self.plot(slope_y_s23, self.gantry_corners[0][1])
ainfo = zip(["z","z1","z2","z3"], z_height[0:4]) ainfo = zip(["z","z1","z2","z3"], z_height[0:4])
apos = " ".join(["%s: %06f" % (x) for x in ainfo]) 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_RTDINLOW = 0x08
MAX31865_FAULT_OVUV = 0x04 MAX31865_FAULT_OVUV = 0x04
VAL_A = 0.00390830 MAX31865_ADC_MAX = 1<<15
VAL_B = 0.0000005775
VAL_C = -0.00000000000418301 # Callendar-Van Dusen constants for platinum resistance thermometers (RTD)
VAL_ADC_MAX = 32768.0 # 2^15 CVD_A = 3.9083e-3
CVD_B = -5.775e-7
class MAX31865(SensorBase): class MAX31865(SensorBase):
def __init__(self, config): def __init__(self, config):
self.rtd_nominal_r = config.getint('rtd_nominal_r', 100) rtd_nominal_r = config.getfloat('rtd_nominal_r', 100., above=0.)
self.reference_r = config.getfloat('rtd_reference_r', 430., 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", SensorBase.__init__(self, config, "MAX31865",
self.build_spi_init(config)) self.build_spi_init(config))
def calc_temp(self, adc, fault): def calc_temp(self, adc, fault):
@ -295,20 +298,20 @@ class MAX31865(SensorBase):
if fault & 0x03: if fault & 0x03:
self.fault("Max31865 Unspecified error") self.fault("Max31865 Unspecified error")
adc = adc >> 1 # remove fault bit adc = adc >> 1 # remove fault bit
R_rtd = (self.reference_r * adc) / VAL_ADC_MAX R_div_nominal = adc * self.adc_to_resist_div_nominal
temp = ((( ( -1 * self.rtd_nominal_r ) * VAL_A ) # Resistance (relative to rtd_nominal_r) is calculated using:
+ math.sqrt( ( self.rtd_nominal_r**2 * VAL_A * VAL_A ) # R_div_nominal = 1. + CVD_A * temp + CVD_B * temp**2
- ( 4 * self.rtd_nominal_r * VAL_B # Solve for temp using quadratic equation:
* ( self.rtd_nominal_r - R_rtd ) ))) # temp = (-b +- sqrt(b**2 - 4ac)) / 2a
/ (2 * self.rtd_nominal_r * VAL_B)) discriminant = math.sqrt(CVD_A**2 - 4. * CVD_B * (1. - R_div_nominal))
temp = (-CVD_A + discriminant) / (2. * CVD_B)
return temp return temp
def calc_adc(self, temp): def calc_adc(self, temp):
R_rtd = temp * ( 2 * self.rtd_nominal_r * VAL_B ) # Calculate relative resistance via Callendar-Van Dusen formula:
R_rtd = math.pow( ( R_rtd + ( self.rtd_nominal_r * VAL_A ) ), 2) # resistance = rtd_nominal_r * (1 + CVD_A * temp + CVD_B * temp**2)
R_rtd = -1 * ( R_rtd - (self.rtd_nominal_r**2 * VAL_A * VAL_A ) ) R_div_nominal = 1. + CVD_A * temp + CVD_B * temp * temp
R_rtd = R_rtd / ( 4 * self.rtd_nominal_r * VAL_B ) adc = int(R_div_nominal / self.adc_to_resist_div_nominal + 0.5)
R_rtd = ( -1 * R_rtd ) + self.rtd_nominal_r adc = max(0, min(MAX31865_ADC_MAX, adc))
adc = int( ( ( R_rtd * VAL_ADC_MAX ) / self.reference_r) + 0.5 )
adc = adc << 1 # Add fault bit adc = adc << 1 # Add fault bit
return adc return adc
def build_spi_init(self, config): def build_spi_init(self, config):

View File

@ -251,7 +251,9 @@ class GCodeDispatch:
raise gcmd.error(self.printer.get_state_message()[0]) raise gcmd.error(self.printer.get_state_message()[0])
return return
if not cmd: if not cmd:
logging.debug(gcmd.get_commandline()) cmdline = gcmd.get_commandline()
if cmdline:
logging.debug(cmdline)
return return
if cmd.startswith("M117 "): if cmd.startswith("M117 "):
# Handle M117 gcode with numeric and special characters # 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: for s, name, spos, epos in end_mcu_pos:
if spos == epos: if spos == epos:
if probe_pos: if probe_pos:
raise EndstopError("Probe triggered prior to movement") raise CommandError("Probe triggered prior to movement")
raise EndstopError( raise CommandError(
"Endstop %s still triggered after retract" % (name,)) "Endstop %s still triggered after retract" % (name,))
def home_rails(self, rails, forcepos, movepos): def home_rails(self, rails, forcepos, movepos):
# Notify of upcoming homing operation # Notify of upcoming homing operation
@ -161,11 +161,4 @@ def multi_complete(printer, completions):
class CommandError(Exception): class CommandError(Exception):
pass 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')) 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. # This file may be distributed under the terms of the GNU GPLv3 license.
import logging import logging
import stepper, homing import stepper
class CartKinematics: class CartKinematics:
def __init__(self, toolhead, config): def __init__(self, toolhead, config):
@ -100,9 +100,8 @@ class CartKinematics:
and (end_pos[i] < self.limits[i][0] and (end_pos[i] < self.limits[i][0]
or end_pos[i] > self.limits[i][1])): or end_pos[i] > self.limits[i][1])):
if self.limits[i][0] > self.limits[i][1]: if self.limits[i][0] > self.limits[i][1]:
raise homing.EndstopMoveError( raise move.move_error("Must home axis first")
end_pos, "Must home axis first") raise move.move_error()
raise homing.EndstopMoveError(end_pos)
def check_move(self, move): def check_move(self, move):
limits = self.limits limits = self.limits
xpos, ypos = move.end_pos[:2] 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. # This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math import logging, math
import stepper, homing import stepper
class CoreXYKinematics: class CoreXYKinematics:
def __init__(self, toolhead, config): def __init__(self, toolhead, config):
@ -77,9 +77,8 @@ class CoreXYKinematics:
and (end_pos[i] < self.limits[i][0] and (end_pos[i] < self.limits[i][0]
or end_pos[i] > self.limits[i][1])): or end_pos[i] > self.limits[i][1])):
if self.limits[i][0] > self.limits[i][1]: if self.limits[i][0] > self.limits[i][1]:
raise homing.EndstopMoveError( raise move.move_error("Must home axis first")
end_pos, "Must home axis first") raise move.move_error()
raise homing.EndstopMoveError(end_pos)
def check_move(self, move): def check_move(self, move):
limits = self.limits limits = self.limits
xpos, ypos = move.end_pos[:2] 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. # This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math import logging, math
import stepper, homing import stepper
class CoreXZKinematics: class CoreXZKinematics:
def __init__(self, toolhead, config): def __init__(self, toolhead, config):
@ -76,9 +76,8 @@ class CoreXZKinematics:
and (end_pos[i] < self.limits[i][0] and (end_pos[i] < self.limits[i][0]
or end_pos[i] > self.limits[i][1])): or end_pos[i] > self.limits[i][1])):
if self.limits[i][0] > self.limits[i][1]: if self.limits[i][0] > self.limits[i][1]:
raise homing.EndstopMoveError( raise move.move_error("Must home axis first")
end_pos, "Must home axis first") raise move.move_error()
raise homing.EndstopMoveError(end_pos)
def check_move(self, move): def check_move(self, move):
limits = self.limits limits = self.limits
xpos, ypos = move.end_pos[:2] 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. # This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging import math, logging
import stepper, homing, mathutil import stepper, mathutil
# Slow moves once the ratio of tower to XY movement exceeds SLOW_RATIO # Slow moves once the ratio of tower to XY movement exceeds SLOW_RATIO
SLOW_RATIO = 3. SLOW_RATIO = 3.
@ -118,7 +118,7 @@ class DeltaKinematics:
# Normal XY move # Normal XY move
return return
if self.need_home: if self.need_home:
raise homing.EndstopMoveError(end_pos, "Must home first") raise move.move_error("Must home first")
end_z = end_pos[2] end_z = end_pos[2]
limit_xy2 = self.max_xy2 limit_xy2 = self.max_xy2
if end_z > self.limit_z: if end_z > self.limit_z:
@ -127,7 +127,7 @@ class DeltaKinematics:
# Move out of range - verify not a homing move # Move out of range - verify not a homing move
if (end_pos[:2] != self.home_position[:2] if (end_pos[:2] != self.home_position[:2]
or end_z < self.min_z or end_z > 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. limit_xy2 = -1.
if move.axes_d[2]: if move.axes_d[2]:
move.limit_speed(self.max_z_velocity, move.accel) 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. # This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging import math, logging
import stepper, homing, chelper import stepper, chelper
class PrinterExtruder: class PrinterExtruder:
def __init__(self, config, extruder_num): def __init__(self, config, extruder_num):
@ -107,13 +107,13 @@ class PrinterExtruder:
def check_move(self, move): def check_move(self, move):
axis_r = move.axes_r[3] axis_r = move.axes_r[3]
if not self.heater.can_extrude: if not self.heater.can_extrude:
raise homing.EndstopError( raise self.printer.command_error(
"Extrude below minimum temp\n" "Extrude below minimum temp\n"
"See the 'min_extrude_temp' config option for details") "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.: 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 # Extrude only move (or retraction move) - limit accel and velocity
if abs(move.axes_d[3]) > self.max_e_dist: 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" "Extrude only move too long (%.3fmm vs %.3fmm)\n"
"See the 'max_extrude_only_distance' config" "See the 'max_extrude_only_distance' config"
" option for details" % (move.axes_d[3], self.max_e_dist)) " option for details" % (move.axes_d[3], self.max_e_dist))
@ -127,7 +127,7 @@ class PrinterExtruder:
area = axis_r * self.filament_area area = axis_r * self.filament_area
logging.debug("Overextrude: %s vs %s (area=%.3f dist=%.3f)", logging.debug("Overextrude: %s vs %s (area=%.3f dist=%.3f)",
axis_r, self.max_extrude_ratio, area, move.move_d) 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" "Move exceeds maximum extrusion (%.3fmm^2 vs %.3fmm^2)\n"
"See the 'max_extrude_cross_section' config option for details" "See the 'max_extrude_cross_section' config option for details"
% (area, self.max_extrude_ratio * self.filament_area)) % (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 # Dummy extruder class used when a printer has no extruder at all
class DummyExtruder: class DummyExtruder:
def __init__(self, printer):
self.printer = printer
def update_move_time(self, flush_time): def update_move_time(self, flush_time):
pass pass
def check_move(self, move): def check_move(self, move):
raise homing.EndstopMoveError( raise move.move_error("Extrude when no extruder present")
move.end_pos, "Extrude when no extruder present")
def calc_junction(self, prev_move, move): def calc_junction(self, prev_move, move):
return move.max_cruise_v2 return move.max_cruise_v2
def get_name(self): def get_name(self):
return "" return ""
def get_heater(self): def get_heater(self):
raise homing.CommandError("Extruder not configured") raise self.printer.command_error("Extruder not configured")
def add_printer_objects(config): def add_printer_objects(config):
printer = config.get_printer() printer = config.get_printer()

View File

@ -4,7 +4,7 @@
# #
# This file may be distributed under the terms of the GNU GPLv3 license. # This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math import logging, math
import stepper, homing import stepper
class PolarKinematics: class PolarKinematics:
def __init__(self, toolhead, config): def __init__(self, toolhead, config):
@ -94,18 +94,17 @@ class PolarKinematics:
xy2 = end_pos[0]**2 + end_pos[1]**2 xy2 = end_pos[0]**2 + end_pos[1]**2
if xy2 > self.limit_xy2: if xy2 > self.limit_xy2:
if self.limit_xy2 < 0.: if self.limit_xy2 < 0.:
raise homing.EndstopMoveError(end_pos, "Must home axis first") raise move.move_error("Must home axis first")
raise homing.EndstopMoveError(end_pos) raise move.move_error()
if move.axes_d[2]: if move.axes_d[2]:
if end_pos[2] < self.limit_z[0] or end_pos[2] > self.limit_z[1]: 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]: if self.limit_z[0] > self.limit_z[1]:
raise homing.EndstopMoveError( raise move.move_error("Must home axis first")
end_pos, "Must home axis first") raise move.move_error()
raise homing.EndstopMoveError(end_pos)
# Move with Z - update velocity and accel for slower Z axis # Move with Z - update velocity and accel for slower Z axis
z_ratio = move.move_d / abs(move.axes_d[2]) z_ratio = move.move_d / abs(move.axes_d[2])
move.limit_speed( move.limit_speed(self.max_z_velocity * z_ratio,
self.max_z_velocity * z_ratio, self.max_z_accel * z_ratio) self.max_z_accel * z_ratio)
def get_status(self, eventtime): def get_status(self, eventtime):
xy_home = "xy" if self.limit_xy2 >= 0. else "" xy_home = "xy" if self.limit_xy2 >= 0. else ""
z_home = "z" if self.limit_z[0] <= self.limit_z[1] 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. # This file may be distributed under the terms of the GNU GPLv3 license.
import math, logging import math, logging
import stepper, homing, mathutil, chelper import stepper, mathutil, chelper
class RotaryDeltaKinematics: class RotaryDeltaKinematics:
def __init__(self, toolhead, config): def __init__(self, toolhead, config):
@ -106,7 +106,7 @@ class RotaryDeltaKinematics:
# Normal XY move # Normal XY move
return return
if self.need_home: if self.need_home:
raise homing.EndstopMoveError(end_pos, "Must home first") raise move.move_error("Must home first")
end_z = end_pos[2] end_z = end_pos[2]
limit_xy2 = self.max_xy2 limit_xy2 = self.max_xy2
if end_z > self.limit_z: if end_z > self.limit_z:
@ -115,7 +115,7 @@ class RotaryDeltaKinematics:
# Move out of range - verify not a homing move # Move out of range - verify not a homing move
if (end_pos[:2] != self.home_position[:2] if (end_pos[:2] != self.home_position[:2]
or end_z < self.min_z or end_z > 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. limit_xy2 = -1.
if move.axes_d[2]: if move.axes_d[2]:
move.limit_speed(self.max_z_velocity, move.accel) move.limit_speed(self.max_z_velocity, move.accel)

View File

@ -1,10 +1,10 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# Main code for host side printer firmware # 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. # 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 util, reactor, queuelogger, msgproto, homing
import gcode, configfile, pins, mcu, toolhead, webhooks import gcode, configfile, pins, mcu, toolhead, webhooks
@ -48,10 +48,10 @@ Printer is shutdown
class Printer: class Printer:
config_error = configfile.error config_error = configfile.error
command_error = homing.CommandError command_error = homing.CommandError
def __init__(self, bglogger, start_args): def __init__(self, main_reactor, bglogger, start_args):
self.bglogger = bglogger self.bglogger = bglogger
self.start_args = start_args self.start_args = start_args
self.reactor = reactor.Reactor() self.reactor = main_reactor
self.reactor.register_callback(self._connect) self.reactor.register_callback(self._connect)
self.state_message = message_startup self.state_message = message_startup
self.in_shutdown_state = False self.in_shutdown_state = False
@ -165,8 +165,8 @@ class Printer:
return return
except Exception as e: except Exception as e:
logging.exception("Unhandled exception during connect") logging.exception("Unhandled exception during connect")
self._set_state("Internal error during connect: %s\n%s" % ( self._set_state("Internal error during connect: %s\n%s"
str(e), message_restart,)) % (str(e), message_restart,))
return return
try: try:
self._set_state(message_ready) self._set_state(message_ready)
@ -176,8 +176,8 @@ class Printer:
cb() cb()
except Exception as e: except Exception as e:
logging.exception("Unhandled exception during ready callback") logging.exception("Unhandled exception during ready callback")
self.invoke_shutdown("Internal error during ready callback: %s" % ( self.invoke_shutdown("Internal error during ready callback: %s"
str(e),)) % (str(e),))
def run(self): def run(self):
systime = time.time() systime = time.time()
monotime = self.reactor.monotonic() monotime = self.reactor.monotonic()
@ -187,8 +187,17 @@ class Printer:
try: try:
self.reactor.run() self.reactor.run()
except: except:
logging.exception("Unhandled exception during run") msg = "Unhandled exception during run"
return "error_exit" 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 # Check restart flags
run_result = self.run_result run_result = self.run_result
try: try:
@ -215,6 +224,8 @@ class Printer:
cb() cb()
except: except:
logging.exception("Exception during shutdown handler") logging.exception("Exception during shutdown handler")
logging.info("Reactor garbage collection: %s",
self.reactor.get_gc_stats())
def invoke_async_shutdown(self, msg): def invoke_async_shutdown(self, msg):
self.reactor.register_async_callback( self.reactor.register_async_callback(
(lambda e: self.invoke_shutdown(msg))) (lambda e: self.invoke_shutdown(msg)))
@ -297,17 +308,22 @@ def main():
elif not options.debugoutput: elif not options.debugoutput:
logging.warning("No log file specified!" logging.warning("No log file specified!"
" Severe timing issues may result!") " Severe timing issues may result!")
gc.disable()
# Start Printer() class # Start Printer() class
while 1: while 1:
if bglogger is not None: if bglogger is not None:
bglogger.clear_rollover_info() bglogger.clear_rollover_info()
bglogger.set_rollover_info('versions', versions) 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() res = printer.run()
if res in ['exit', 'error_exit']: if res in ['exit', 'error_exit']:
break break
time.sleep(1.) time.sleep(1.)
main_reactor.finalize()
main_reactor = printer = None
logging.info("Restarting printer") logging.info("Restarting printer")
start_args['start_reason'] = res start_args['start_reason'] = res

View File

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

View File

@ -1,9 +1,9 @@
# File descriptor and timer event helper # 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. # 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 greenlet
import chelper, util import chelper, util
@ -93,10 +93,13 @@ class ReactorMutex:
class SelectReactor: class SelectReactor:
NOW = _NOW NOW = _NOW
NEVER = _NEVER NEVER = _NEVER
def __init__(self): def __init__(self, gc_checking=False):
# Main code # Main code
self._process = False self._process = False
self.monotonic = chelper.get_ffi()[1].get_monotonic self.monotonic = chelper.get_ffi()[1].get_monotonic
# Python garbage collection
self._check_gc = gc_checking
self._last_gc_times = [0., 0., 0.]
# Timers # Timers
self._timers = [] self._timers = []
self._next_timer = self.NEVER self._next_timer = self.NEVER
@ -108,6 +111,9 @@ class SelectReactor:
# Greenlets # Greenlets
self._g_dispatch = None self._g_dispatch = None
self._greenlets = [] self._greenlets = []
self._all_greenlets = []
def get_gc_stats(self):
return tuple(self._last_gc_times)
# Timers # Timers
def update_timer(self, timer_handler, waketime): def update_timer(self, timer_handler, waketime):
timer_handler.waketime = waketime timer_handler.waketime = waketime
@ -124,8 +130,22 @@ class SelectReactor:
timers = list(self._timers) timers = list(self._timers)
timers.pop(timers.index(timer_handler)) timers.pop(timers.index(timer_handler))
self._timers = timers self._timers = timers
def _check_timers(self, eventtime): def _check_timers(self, eventtime, busy):
if eventtime < self._next_timer: 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)) return min(1., max(.001, self._next_timer - eventtime))
self._next_timer = self.NEVER self._next_timer = self.NEVER
g_dispatch = self._g_dispatch g_dispatch = self._g_dispatch
@ -139,9 +159,7 @@ class SelectReactor:
self._end_greenlet(g_dispatch) self._end_greenlet(g_dispatch)
return 0. return 0.
self._next_timer = min(self._next_timer, waketime) self._next_timer = min(self._next_timer, waketime)
if eventtime >= self._next_timer: return 0.
return 0.
return min(1., max(.001, self._next_timer - self.monotonic()))
# Callbacks and Completions # Callbacks and Completions
def completion(self): def completion(self):
return ReactorCompletion(self) return ReactorCompletion(self)
@ -178,11 +196,6 @@ class SelectReactor:
util.set_nonblock(self._pipe_fds[0]) util.set_nonblock(self._pipe_fds[0])
util.set_nonblock(self._pipe_fds[1]) util.set_nonblock(self._pipe_fds[1])
self.register_fd(self._pipe_fds[0], self._got_pipe_signal) 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 # Greenlets
def _sys_pause(self, waketime): def _sys_pause(self, waketime):
# Pause using system sleep for when reactor not running # Pause using system sleep for when reactor not running
@ -202,6 +215,7 @@ class SelectReactor:
g_next = self._greenlets.pop() g_next = self._greenlets.pop()
else: else:
g_next = ReactorGreenlet(run=self._dispatch_loop) g_next = ReactorGreenlet(run=self._dispatch_loop)
self._all_greenlets.append(g_next)
g_next.parent = g.parent g_next.parent = g.parent
g.timer = self.register_timer(g.switch, waketime) g.timer = self.register_timer(g.switch, waketime)
self._next_timer = self.NOW self._next_timer = self.NOW
@ -231,12 +245,15 @@ class SelectReactor:
# Main loop # Main loop
def _dispatch_loop(self): def _dispatch_loop(self):
self._g_dispatch = g_dispatch = greenlet.getcurrent() self._g_dispatch = g_dispatch = greenlet.getcurrent()
busy = True
eventtime = self.monotonic() eventtime = self.monotonic()
while self._process: while self._process:
timeout = self._check_timers(eventtime) timeout = self._check_timers(eventtime, busy)
busy = False
res = select.select(self._fds, [], [], timeout) res = select.select(self._fds, [], [], timeout)
eventtime = self.monotonic() eventtime = self.monotonic()
for fd in res[0]: for fd in res[0]:
busy = True
fd.callback(eventtime) fd.callback(eventtime)
if g_dispatch is not self._g_dispatch: if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch) self._end_greenlet(g_dispatch)
@ -248,13 +265,27 @@ class SelectReactor:
self._setup_async_callbacks() self._setup_async_callbacks()
self._process = True self._process = True
g_next = ReactorGreenlet(run=self._dispatch_loop) g_next = ReactorGreenlet(run=self._dispatch_loop)
self._all_greenlets.append(g_next)
g_next.switch() g_next.switch()
def end(self): def end(self):
self._process = False 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): class PollReactor(SelectReactor):
def __init__(self): def __init__(self, gc_checking=False):
SelectReactor.__init__(self) SelectReactor.__init__(self, gc_checking)
self._poll = select.poll() self._poll = select.poll()
self._fds = {} self._fds = {}
# File descriptors # File descriptors
@ -273,12 +304,15 @@ class PollReactor(SelectReactor):
# Main loop # Main loop
def _dispatch_loop(self): def _dispatch_loop(self):
self._g_dispatch = g_dispatch = greenlet.getcurrent() self._g_dispatch = g_dispatch = greenlet.getcurrent()
busy = True
eventtime = self.monotonic() eventtime = self.monotonic()
while self._process: 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.))) res = self._poll.poll(int(math.ceil(timeout * 1000.)))
eventtime = self.monotonic() eventtime = self.monotonic()
for fd, event in res: for fd, event in res:
busy = True
self._fds[fd](eventtime) self._fds[fd](eventtime)
if g_dispatch is not self._g_dispatch: if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch) self._end_greenlet(g_dispatch)
@ -287,8 +321,8 @@ class PollReactor(SelectReactor):
self._g_dispatch = None self._g_dispatch = None
class EPollReactor(SelectReactor): class EPollReactor(SelectReactor):
def __init__(self): def __init__(self, gc_checking=False):
SelectReactor.__init__(self) SelectReactor.__init__(self, gc_checking)
self._epoll = select.epoll() self._epoll = select.epoll()
self._fds = {} self._fds = {}
# File descriptors # File descriptors
@ -307,12 +341,15 @@ class EPollReactor(SelectReactor):
# Main loop # Main loop
def _dispatch_loop(self): def _dispatch_loop(self):
self._g_dispatch = g_dispatch = greenlet.getcurrent() self._g_dispatch = g_dispatch = greenlet.getcurrent()
busy = True
eventtime = self.monotonic() eventtime = self.monotonic()
while self._process: while self._process:
timeout = self._check_timers(eventtime) timeout = self._check_timers(eventtime, busy)
busy = False
res = self._epoll.poll(timeout) res = self._epoll.poll(timeout)
eventtime = self.monotonic() eventtime = self.monotonic()
for fd, event in res: for fd, event in res:
busy = True
self._fds[fd](eventtime) self._fds[fd](eventtime)
if g_dispatch is not self._g_dispatch: if g_dispatch is not self._g_dispatch:
self._end_greenlet(g_dispatch) self._end_greenlet(g_dispatch)

View File

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

View File

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

View File

@ -54,6 +54,10 @@ class Move:
self.accel = min(self.accel, accel) self.accel = min(self.accel, accel)
self.delta_v2 = 2.0 * self.move_d * self.accel self.delta_v2 = 2.0 * self.move_d * self.accel
self.smooth_delta_v2 = min(self.smooth_delta_v2, self.delta_v2) 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): def calc_junction(self, prev_move):
if not self.is_kinematic_move or not prev_move.is_kinematic_move: if not self.is_kinematic_move or not prev_move.is_kinematic_move:
return return
@ -246,7 +250,7 @@ class ToolHead:
self.trapq_free_moves = ffi_lib.trapq_free_moves self.trapq_free_moves = ffi_lib.trapq_free_moves
self.step_generators = [] self.step_generators = []
# Create kinematics class # Create kinematics class
self.extruder = kinematics.extruder.DummyExtruder() self.extruder = kinematics.extruder.DummyExtruder(self.printer)
kin_name = config.get('kinematics') kin_name = config.get('kinematics')
try: try:
mod = importlib.import_module('kinematics.' + kin_name) mod = importlib.import_module('kinematics.' + kin_name)
@ -466,7 +470,7 @@ class ToolHead:
# Submit move # Submit move
try: try:
self.move(newpos, speed) self.move(newpos, speed)
except homing.CommandError as e: except self.printer.command_error as e:
self.flush_step_generation() self.flush_step_generation()
raise raise
# Transmit move in "drip" mode # Transmit move in "drip" mode

View File

@ -61,7 +61,8 @@ class WebRequest:
value = self.params.get(item, default) value = self.params.get(item, default)
if value is Sentinel: if value is Sentinel:
raise WebRequestError("Missing Argument [%s]" % (item,)) 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,)) raise WebRequestError("Invalid Argument Type [%s]" % (item,))
return value return value
@ -168,19 +169,30 @@ class ClientConnection:
self.sock.fileno(), self.process_received) self.sock.fileno(), self.process_received)
self.partial_data = self.send_buffer = "" self.partial_data = self.send_buffer = ""
self.is_sending_data = False self.is_sending_data = False
logging.info( self.set_client_info("?", "New connection")
"webhooks: New connection established")
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): def close(self):
if self.fd_handle is not None: if self.fd_handle is None:
logging.info("webhooks: Client connection closed") return
self.reactor.unregister_fd(self.fd_handle) self.set_client_info(None, "Disconnected")
self.fd_handle = None self.reactor.unregister_fd(self.fd_handle)
try: self.fd_handle = None
self.sock.close() try:
except socket.error: self.sock.close()
pass except socket.error:
self.server.pop_client(self.uid) pass
self.server.pop_client(self.uid)
def is_closed(self): def is_closed(self):
return self.fd_handle is None return self.fd_handle is None
@ -216,7 +228,7 @@ class ClientConnection:
try: try:
func = self.webhooks.get_callback(web_request.get_method()) func = self.webhooks.get_callback(web_request.get_method())
func(web_request) func(web_request)
except homing.CommandError as e: except self.printer.command_error as e:
web_request.set_error(WebRequestError(e.message)) web_request.set_error(WebRequestError(e.message))
except Exception as e: except Exception as e:
msg = ("Internal Error on WebRequest: %s" msg = ("Internal Error on WebRequest: %s"
@ -276,9 +288,12 @@ class WebHooks:
web_request.send({'endpoints': self._endpoints.keys()}) web_request.send({'endpoints': self._endpoints.keys()})
def _handle_info_request(self, web_request): 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() state_message, state = self.printer.get_state_message()
klipper_path = os.path.normpath(os.path.join( src_path = os.path.dirname(__file__)
os.path.dirname(__file__), "..")) klipper_path = os.path.normpath(os.path.join(src_path, ".."))
response = {'state': state, 'state_message': state_message, response = {'state': state, 'state_message': state_message,
'hostname': socket.gethostname(), 'hostname': socket.gethostname(),
'klipper_path': klipper_path, 'python_path': sys.executable} '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. # Script to interact with simulavr by simulating a serial port.
# #
# Copyright (C) 2015-2018 Kevin O'Connor <kevin@koconnor.net> # Copyright (C) 2015-2018 Kevin O'Connor <kevin@koconnor.net>
@ -17,7 +17,7 @@ class SerialRxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
pysimulavr.PySimulationMember.__init__(self) pysimulavr.PySimulationMember.__init__(self)
self.terminal = terminal self.terminal = terminal
self.sc = pysimulavr.SystemClock.Instance() self.sc = pysimulavr.SystemClock.Instance()
self.delay = SIMULAVR_FREQ / baud self.delay = SIMULAVR_FREQ // baud
self.current = 0 self.current = 0
self.pos = -1 self.pos = -1
def SetInState(self, pin): def SetInState(self, pin):
@ -33,7 +33,7 @@ class SerialRxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
if self.pos == 1: if self.pos == 1:
return int(self.delay * 1.5) return int(self.delay * 1.5)
if self.pos >= SERIALBITS: if self.pos >= SERIALBITS:
data = chr((self.current >> 1) & 0xff) data = bytearray([(self.current >> 1) & 0xff])
self.terminal.write(data) self.terminal.write(data)
self.pos = -1 self.pos = -1
self.current = 0 self.current = 0
@ -48,10 +48,10 @@ class SerialTxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
self.terminal = terminal self.terminal = terminal
self.SetPin('H') self.SetPin('H')
self.sc = pysimulavr.SystemClock.Instance() self.sc = pysimulavr.SystemClock.Instance()
self.delay = SIMULAVR_FREQ / baud self.delay = SIMULAVR_FREQ // baud
self.current = 0 self.current = 0
self.pos = 0 self.pos = 0
self.queue = "" self.queue = bytearray()
self.sc.Add(self) self.sc.Add(self)
def DoStep(self, trueHwStep): def DoStep(self, trueHwStep):
if not self.pos: if not self.pos:
@ -59,9 +59,8 @@ class SerialTxPin(pysimulavr.PySimulationMember, pysimulavr.Pin):
data = self.terminal.read() data = self.terminal.read()
if not data: if not data:
return self.delay * 100 return self.delay * 100
self.queue += data self.queue.extend(data)
self.current = (ord(self.queue[0]) << 1) | 0x200 self.current = (self.queue.pop(0) << 1) | 0x200
self.queue = self.queue[1:]
newstate = 'L' newstate = 'L'
if self.current & (1 << self.pos): if self.current & (1 << self.pos):
newstate = 'H' newstate = 'H'
@ -109,7 +108,7 @@ class Pacing(pysimulavr.PySimulationMember):
self.next_check_clock = 0 self.next_check_clock = 0
self.rel_time = time.time() self.rel_time = time.time()
self.best_offset = 0. self.best_offset = 0.
self.delay = SIMULAVR_FREQ / 10000 self.delay = SIMULAVR_FREQ // 10000
self.sc.Add(self) self.sc.Add(self)
def DoStep(self, trueHwStep): def DoStep(self, trueHwStep):
curtime = time.time() curtime = time.time()
@ -135,7 +134,7 @@ class TerminalIO:
def read(self): def read(self):
try: try:
return os.read(self.fd, 64) return os.read(self.fd, 64)
except os.error, e: except os.error as e:
if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK): if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):
pysimulavr.SystemClock.Instance().stop() pysimulavr.SystemClock.Instance().stop()
return "" return ""
@ -198,7 +197,7 @@ def main():
trace = Tracing(options.tracefile, options.trace) trace = Tracing(options.tracefile, options.trace)
dev = pysimulavr.AvrFactory.instance().makeDevice(proc) dev = pysimulavr.AvrFactory.instance().makeDevice(proc)
dev.Load(elffile) dev.Load(elffile)
dev.SetClockFreq(SIMULAVR_FREQ / speed) dev.SetClockFreq(SIMULAVR_FREQ // speed)
sc.Add(dev) sc.Add(dev)
pysimulavr.cvar.sysConHandler.SetUseExit(False) pysimulavr.cvar.sysConHandler.SetUseExit(False)
trace.load_options() 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() fig.tight_layout()
return fig 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 # Startup
@ -111,6 +132,8 @@ def main():
default=5., help="pullup resistor") default=5., help="pullup resistor")
opts.add_option("-s", "--sensors", type="string", dest="sensors", opts.add_option("-s", "--sensors", type="string", dest="sensors",
default="", help="list of sensors (comma separated)") default="", help="list of sensors (comma separated)")
opts.add_option("-r", "--resistance", action="store_true",
help="graph sensor resistance")
options, args = opts.parse_args() options, args = opts.parse_args()
if len(args) != 0: if len(args) != 0:
opts.error("Incorrect number of arguments") opts.error("Incorrect number of arguments")
@ -129,7 +152,10 @@ def main():
# Draw graph # Draw graph
setup_matplotlib(options.output is not None) 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 # Show graph
if options.output is None: if options.output is None:

View File

@ -137,6 +137,48 @@ def plot_mcu(data, maxbw):
ax1.grid(True) ax1.grid(True)
return fig 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): def plot_frequency(data, mcu):
all_keys = {} all_keys = {}
for d in data: for d in data:
@ -211,6 +253,8 @@ def main():
opts = optparse.OptionParser(usage) opts = optparse.OptionParser(usage)
opts.add_option("-f", "--frequency", action="store_true", opts.add_option("-f", "--frequency", action="store_true",
help="graph mcu frequency") 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", opts.add_option("-o", "--output", type="string", dest="output",
default=None, help="filename of output graph") default=None, help="filename of output graph")
opts.add_option("-t", "--temperature", type="string", dest="heater", opts.add_option("-t", "--temperature", type="string", dest="heater",
@ -233,6 +277,8 @@ def main():
fig = plot_temperature(data, options.heater) fig = plot_temperature(data, options.heater)
elif options.frequency: elif options.frequency:
fig = plot_frequency(data, options.mcu) fig = plot_frequency(data, options.mcu)
elif options.system:
fig = plot_system(data)
else: else:
fig = plot_mcu(data, MAXBANDWIDTH) fig = plot_mcu(data, MAXBANDWIDTH)

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ create_virtualenv()
report_status "Updating python virtual environment..." report_status "Updating python virtual environment..."
# Create virtualenv if it doesn't already exist # Create virtualenv if it doesn't already exist
[ ! -d ${PYTHONDIR} ] && virtualenv ${PYTHONDIR} [ ! -d ${PYTHONDIR} ] && virtualenv -p python2 ${PYTHONDIR}
# Install/update dependencies # Install/update dependencies
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt ${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" echo -e "\n\n=============== Install python virtualenv\n\n"
cd ${MAIN_DIR} 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 ${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_I2C) += i2ccmds.c
src-$(CONFIG_HAVE_GPIO_HARD_PWM) += pwmcmds.c src-$(CONFIG_HAVE_GPIO_HARD_PWM) += pwmcmds.c
src-$(CONFIG_HAVE_GPIO_BITBANGING) += lcd_st7920.c lcd_hd44780.c buttons.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 bool "28KiB bootloader" if MACH_STM32F103
config STM32_FLASH_START_8000 config STM32_FLASH_START_8000
bool "32KiB bootloader (SKR-PRO or TFT35-V3.0)" if MACH_STM32F207 || MACH_STM32F407 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 config STM32_FLASH_START_10000
bool "64KiB bootloader (Alfawise)" if MACH_STM32F103 bool "64KiB bootloader (Alfawise)" if MACH_STM32F103
config STM32_FLASH_START_0000 config STM32_FLASH_START_0000
@ -129,6 +131,7 @@ config FLASH_START
default 0x8005000 if STM32_FLASH_START_5000 default 0x8005000 if STM32_FLASH_START_5000
default 0x8007000 if STM32_FLASH_START_7000 default 0x8007000 if STM32_FLASH_START_7000
default 0x8008000 if STM32_FLASH_START_8000 default 0x8008000 if STM32_FLASH_START_8000
default 0x8008800 if STM32_FLASH_START_8800
default 0x8010000 if STM32_FLASH_START_10000 default 0x8010000 if STM32_FLASH_START_10000
default 0x8000000 default 0x8000000

View File

@ -127,6 +127,8 @@ CONFIG ../../config/generic-mks-robin-e3.cfg
CONFIG ../../config/generic-mks-robin-nano.cfg CONFIG ../../config/generic-mks-robin-nano.cfg
CONFIG ../../config/printer-alfawise-u30-2018.cfg CONFIG ../../config/printer-alfawise-u30-2018.cfg
CONFIG ../../config/printer-creality-ender3-v2-2020.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 CONFIG ../../config/printer-twotrees-sapphire-pro-2020.cfg
# Printers using the stm32f407 # Printers using the stm32f407