First commit

This commit is contained in:
Cyril 2024-03-10 21:45:05 +01:00
commit 7693c29676
102 changed files with 11831 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,14 @@
########################################
# Buzzer Support
########################################
[gcode_shell_command beep]
command: aplay /usr/data/helper-script/files/buzzer-support/beep.mp3
timeout: 2
verbose: False
[gcode_macro BEEP]
gcode:
RUN_SHELL_COMMAND CMD=beep
RUN_SHELL_COMMAND CMD=beep
RUN_SHELL_COMMAND CMD=beep

View file

@ -0,0 +1,115 @@
########################################
# Camera Settings Control
########################################
[delayed_gcode LOAD_CAM_SETTINGS]
initial_duration: 2
gcode:
CAM_BRIGHTNESS BRIGHTNESS=0
CAM_CONTRAST CONTRAST=32
CAM_SATURATION SATURATION=56
CAM_HUE HUE=0
CAM_WHITE_BALANCE_TEMPERATURE_AUTO WHITE_BALANCE_TEMPERATURE_AUTO=1
CAM_GAMMA GAMMA=80
CAM_GAIN GAIN=0
CAM_POWER_LINE_FREQUENCY POWER_LINE_FREQUENCY=1
CAM_WHITE_BALANCE_TEMPERATURE WHITE_BALANCE_TEMPERATURE=4600
CAM_SHARPNESS SHARPNESS=3
CAM_BACKLIGHT_COMPENSATION BACKLIGHT_COMPENSATION=1
CAM_EXPOSURE_AUTO EXPOSURE_AUTO=3
CAM_EXPOSURE_AUTO_PRIORITY EXPOSURE_AUTO_PRIORITY=0
CAM_AUTO_FOCUS FOCUS_AUTO=0
[gcode_shell_command v4l2-ctl]
command: v4l2-ctl
timeout: 5.0
verbose: True
[gcode_macro CAM_SETTINGS]
gcode:
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 -l"
[gcode_macro CAM_BRIGHTNESS]
description: min=-64 / max=64
gcode:
{% set brightness = params.BRIGHTNESS|default(0) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl brightness="{brightness}
[gcode_macro CAM_CONTRAST]
description: min=0 / max=64
gcode:
{% set contrast = params.CONTRAST|default(32) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl contrast="{contrast}
[gcode_macro CAM_SATURATION]
description: min=0 / max=128
gcode:
{% set saturation = params.SATURATION|default(56) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl saturation="{saturation}
[gcode_macro CAM_HUE]
description: min=-40 / max=40
gcode:
{% set hue = params.HUE|default(0) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl hue="{hue}
[gcode_macro CAM_WHITE_BALANCE_TEMPERATURE_AUTO]
description: disable=0 / enable=1
gcode:
{% set white_balance_temperature_auto = params.WHITE_BALANCE_TEMPERATURE_AUTO|default(1) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl white_balance_temperature_auto="{white_balance_temperature_auto}
[gcode_macro CAM_GAMMA]
description: min=72 / max=500
gcode:
{% set gamma = params.GAMMA|default(80) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl gamma="{gamma}
[gcode_macro CAM_GAIN]
description: min=0 / max=100
gcode:
{% set gain = params.GAIN|default(0) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl gain="{gain}
[gcode_macro CAM_POWER_LINE_FREQUENCY]
description: min=0 / max=2
gcode:
{% set power_line_frequency = params.POWER_LINE_FREQUENCY|default(1) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl power_line_frequency="{power_line_frequency}
[gcode_macro CAM_WHITE_BALANCE_TEMPERATURE]
description: min=2800 / max=6500
gcode:
{% set white_balance_temperature = params.WHITE_BALANCE_TEMPERATURE|default(4600) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl white_balance_temperature="{white_balance_temperature}
[gcode_macro CAM_SHARPNESS]
description: min=0 / max=6
gcode:
{% set sharpness = params.SHARPNESS|default(3) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl sharpness="{sharpness}
[gcode_macro CAM_BACKLIGHT_COMPENSATION]
description: min=0 / max=2
gcode:
{% set backlight_compensation = params.BACKLIGHT_COMPENSATION|default(1) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl backlight_compensation="{backlight_compensation}
[gcode_macro CAM_EXPOSURE_AUTO]
description: manual=1 / auto=3
gcode:
{% set exposure_auto = params.EXPOSURE_AUTO|default(3) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl exposure_auto="{exposure_auto}
[gcode_macro CAM_EXPOSURE_AUTO_PRIORITY]
description: disable=0 / enable=1
gcode:
{% set exposure_auto_priority = params.EXPOSURE_AUTO_PRIORITY|default(0) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl exposure_auto_priority="{exposure_auto_priority}
[gcode_macro CAM_AUTO_FOCUS]
description: disable=0 / enable=1
gcode:
{% set focus_auto = params.AUTO_FOCUS|default(0) %}
RUN_SHELL_COMMAND CMD=v4l2-ctl PARAMS="-d /dev/video4 --set-ctrl focus_auto="{focus_auto}

58
files/entware/generic.sh Executable file
View file

@ -0,0 +1,58 @@
#!/bin/sh
unset LD_LIBRARY_PATH
unset LD_PRELOAD
LOADER=ld.so.1
GLIBC=2.27
echo -e "Info: Removing old directories..."
rm -rf /opt
rm -rf /usr/data/opt
echo -e "Info: Creating directory..."
mkdir -p /usr/data/opt
echo -e "Info: Linking folder..."
ln -nsf /usr/data/opt /opt
echo -e "Info: Creating subdirectories..."
for folder in bin etc lib/opkg tmp var/lock
do
mkdir -p /usr/data/opt/$folder
done
echo -e "Info: Downloading opkg package manager..."
chmod 755 /usr/data/helper-script/files/fixes/curl
URL="http://www.openk1.org/static/entware/mipselsf-k3.4/installer"
/usr/data/helper-script/files/fixes/curl -L "$URL/opkg" -o "/opt/bin/opkg"
/usr/data/helper-script/files/fixes/curl -L "$URL/opkg.conf" -o "/opt/etc/opkg.conf"
echo -e "Info: Applying permissions..."
chmod 755 /opt/bin/opkg
chmod 777 /opt/tmp
echo -e "Info: Installing basic packages..."
/opt/bin/opkg update
/opt/bin/opkg install entware-opt
echo -e "Info: Installing SFTP server support..."
/opt/bin/opkg install openssh-sftp-server; ln -s /opt/libexec/sftp-server /usr/libexec/sftp-server
echo -e "Info: Configuring files..."
for file in passwd group shells shadow gshadow; do
if [ -f /etc/$file ]; then
ln -sf /etc/$file /opt/etc/$file
else
[ -f /opt/etc/$file.1 ] && cp /opt/etc/$file.1 /opt/etc/$file
fi
done
[ -f /etc/localtime ] && ln -sf /etc/localtime /opt/etc/localtime
echo -e "Info: Applying changes in system profile..."
echo 'export PATH="/opt/bin:/opt/sbin:$PATH"' > /etc/profile.d/entware.sh
echo -e "Info: Adding startup script..."
echo '#!/bin/sh\n/opt/etc/init.d/rc.unslung "$1"' > /etc/init.d/S50unslung
chmod 755 /etc/init.d/S50unslung

BIN
files/fixes/curl Executable file

Binary file not shown.

549
files/fixes/gcode.py Normal file
View file

@ -0,0 +1,549 @@
# Parse gcode commands
#
# Copyright (C) 2016-2021 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import os, re, logging, collections, shlex
from extras.tool import reportInformation
class CommandError(Exception):
pass
Coord = collections.namedtuple('Coord', ('x', 'y', 'z', 'e'))
class GCodeCommand:
error = CommandError
def __init__(self, gcode, command, commandline, params, need_ack):
self._command = command
self._commandline = commandline
self._params = params
self._need_ack = need_ack
# Method wrappers
self.respond_info = gcode.respond_info
self.respond_raw = gcode.respond_raw
def get_command(self):
return self._command
def get_commandline(self):
return self._commandline
def get_command_parameters(self):
return self._params
def get_raw_command_parameters(self):
command = self._command
if command.startswith("M117 ") or command.startswith("M118 "):
command = command[:4]
rawparams = self._commandline
urawparams = rawparams.upper()
if not urawparams.startswith(command):
rawparams = rawparams[urawparams.find(command):]
end = rawparams.rfind('*')
if end >= 0:
rawparams = rawparams[:end]
rawparams = rawparams[len(command):]
if rawparams.startswith(' '):
rawparams = rawparams[1:]
return rawparams
def ack(self, msg=None):
if not self._need_ack:
return False
ok_msg = "ok"
if msg:
ok_msg = "ok %s" % (msg,)
self.respond_raw(ok_msg)
self._need_ack = False
return True
# Parameter parsing helpers
class sentinel: pass
def get(self, name, default=sentinel, parser=str, minval=None, maxval=None,
above=None, below=None):
value = self._params.get(name)
if value is None:
if default is self.sentinel:
raise self.error("""{"code":"key251", "msg":"Error on '%s': missing %s", "values":["%s",%s"]}"""
% (self._commandline, name, self._commandline, name))
return default
try:
value = parser(value)
except:
raise self.error(
"""{"code":"key171", "msg": "Unable to parse '%s' as a %s", "values": ["%s", "%s"]}""" % (self._commandline, value,
self._commandline, value)
)
if minval is not None and value < minval:
raise self.error("""{"code":"key252","msg":"Error on '%s': %s must have minimum of %s","values":["%s","%s","%s"]}"""
% (self._commandline, name, minval, self._commandline, name, minval))
if maxval is not None and value > maxval:
raise self.error("""{"code":"key253", "msg":"Error on '%s': %s must have maximumof %s", "values":["%s","%s","%s"]}"""
% (self._commandline, name, maxval, self._commandline, name, maxval))
if above is not None and value <= above:
raise self.error("""{"code":"key254", "msg":"Error on '%s': %s must be above %s", "values":["%s","%s","%s"]}"""
% (self._commandline, name, above, self._commandline, name, above))
if below is not None and value >= below:
raise self.error("""{"code":"key255", "msg":"Error on '%s': %s must be below %s", "values":["%s","%s","%s"]}"""
% (self._commandline, name, below, self._commandline, name, below))
return value
def get_int(self, name, default=sentinel, minval=None, maxval=None):
return self.get(name, default, parser=int, minval=minval, maxval=maxval)
def get_float(self, name, default=sentinel, minval=None, maxval=None,
above=None, below=None):
return self.get(name, default, parser=float, minval=minval,
maxval=maxval, above=above, below=below)
# Parse and dispatch G-Code commands
class GCodeDispatch:
error = CommandError
Coord = Coord
def __init__(self, printer):
self.printer = printer
self.is_fileinput = not not printer.get_start_args().get("debuginput")
printer.register_event_handler("klippy:ready", self._handle_ready)
printer.register_event_handler("klippy:shutdown", self._handle_shutdown)
printer.register_event_handler("klippy:disconnect",
self._handle_disconnect)
# Command handling
self.is_printer_ready = False
self.mutex = printer.get_reactor().mutex()
self.output_callbacks = []
self.base_gcode_handlers = self.gcode_handlers = {}
self.ready_gcode_handlers = {}
self.mux_commands = {}
self.gcode_help = {}
# Register commands needed before config file is loaded
handlers = ['M110', 'M112', 'M115',
'RESTART', 'FIRMWARE_RESTART', 'ECHO', 'STATUS', 'HELP']
for cmd in handlers:
func = getattr(self, 'cmd_' + cmd)
desc = getattr(self, 'cmd_' + cmd + '_help', None)
self.register_command(cmd, func, True, desc)
self.last_temperature_info = "/usr/data/creality/userdata/config/temperature_info.json"
self.exclude_object_info = "/usr/data/creality/userdata/config/exclude_object_info.json"
def is_traditional_gcode(self, cmd):
# A "traditional" g-code command is a letter and followed by a number
try:
cmd = cmd.upper().split()[0]
val = float(cmd[1:])
return cmd[0].isupper() and cmd[1].isdigit()
except:
return False
def register_command(self, cmd, func, when_not_ready=False, desc=None):
if func is None:
old_cmd = self.ready_gcode_handlers.get(cmd)
if cmd in self.ready_gcode_handlers:
del self.ready_gcode_handlers[cmd]
if cmd in self.base_gcode_handlers:
del self.base_gcode_handlers[cmd]
return old_cmd
if cmd in self.ready_gcode_handlers:
raise self.printer.config_error(
"""{"code":"key57", "msg":"gcode command %s already registered", "values": ["%s"]}""" % (cmd, cmd))
if not self.is_traditional_gcode(cmd):
origfunc = func
func = lambda params: origfunc(self._get_extended_params(params))
self.ready_gcode_handlers[cmd] = func
if when_not_ready:
self.base_gcode_handlers[cmd] = func
if desc is not None:
self.gcode_help[cmd] = desc
def register_mux_command(self, cmd, key, value, func, desc=None):
prev = self.mux_commands.get(cmd)
if prev is None:
handler = lambda gcmd: self._cmd_mux(cmd, gcmd)
self.register_command(cmd, handler, desc=desc)
self.mux_commands[cmd] = prev = (key, {})
prev_key, prev_values = prev
if prev_key != key:
raise self.printer.config_error(
"""{"code":"key58", "msg":"mux command %s %s %s may have only one key (%s)", "values": ["%s", "%s", "%s", "%s"]}""" % (
cmd, key, value, prev_key, cmd, key, value, prev_key))
if value in prev_values:
raise self.printer.config_error(
"""{"code":"key59", "msg":"mux command %s %s %s already registered (%s)", "values": ["%s", "%s", "%s", "%s"]}""" % (
cmd, key, value, prev_values, cmd, key, value, prev_values))
prev_values[value] = func
def get_command_help(self):
return dict(self.gcode_help)
def register_output_handler(self, cb):
self.output_callbacks.append(cb)
def _handle_shutdown(self):
if not self.is_printer_ready:
return
self.is_printer_ready = False
self.gcode_handlers = self.base_gcode_handlers
self._respond_state("Shutdown")
def _handle_disconnect(self):
self._respond_state("Disconnect")
def _handle_ready(self):
self.is_printer_ready = True
self.gcode_handlers = self.ready_gcode_handlers
self._respond_state("Ready")
# Parse input into commands
args_r = re.compile('([A-Z_]+|[A-Z*/])')
def _process_commands(self, commands, need_ack=True):
for line in commands:
# Ignore comments and leading/trailing spaces
line = origline = line.strip()
cpos = line.find(';')
if cpos >= 0:
line = line[:cpos]
# Break line into parts and determine command
parts = self.args_r.split(line.upper())
numparts = len(parts)
cmd = ""
if numparts >= 3 and parts[1] != 'N':
cmd = parts[1] + parts[2].strip()
elif numparts >= 5 and parts[1] == 'N':
# Skip line number at start of command
cmd = parts[3] + parts[4].strip()
# Build gcode "params" dictionary
params = { parts[i]: parts[i+1].strip()
for i in range(1, numparts, 2) }
gcmd = GCodeCommand(self, cmd, origline, params, need_ack)
# Invoke handler for command
handler = self.gcode_handlers.get(cmd, self.cmd_default)
try:
handler(gcmd)
except self.error as e:
self._respond_error(str(e))
self.printer.send_event("gcode:command_error")
if not need_ack:
raise
except:
msg = """{"code":"key60", "msg":"Internal error on command:%s", "values": ["%s"]}""" % (cmd, cmd)
logging.exception(msg)
self.printer.invoke_shutdown(msg)
self._respond_error(msg)
if not need_ack:
raise
gcmd.ack()
if line.startswith("G1") or line.startswith("G0"):
pass
elif line.startswith("M104"):
self.set_temperature("extruder", line)
elif line.startswith("M140"):
self.set_temperature("bed", line)
elif line.startswith("M109"):
self.set_temperature("extruder", line)
elif line.startswith("M190"):
self.set_temperature("bed", line)
elif line.startswith("EXCLUDE_OBJECT_DEFINE") or line.startswith("EXCLUDE_OBJECT NAME"):
self.record_exclude_object_info(line)
def set_temperature(self, key, value):
import json
try:
# configfile = self.printer.lookup_object('configfile')
# print_stats = self.printer.load_object(configfile, 'print_stats')
temp_value = float(value.strip("\n").split("S")[-1])
# if key == "extruder" and print_stats and print_stats.state == "printing":
# if temp_value >= 240:
# self.run_script_from_command("M107 P1")
# logging.info("Fan Off SET M107 P1")
# elif temp_value >= 170:
# self.run_script_from_command("M106 P1 S255")
# logging.info("Fan On SET M106 P1 S255")
if key == "extruder" and temp_value < 170:
return
if not os.path.exists(self.last_temperature_info):
from subprocess import call
call("touch %s" % self.last_temperature_info, shell=True)
with open(self.last_temperature_info, "r") as f:
ret = f.read()
if len(ret) > 0:
ret = json.loads(ret)
else:
ret = {}
ret[key] = temp_value
with open(self.last_temperature_info, "w") as f:
f.write(json.dumps(ret))
f.flush()
except Exception as err:
logging.error("set_temperature error: %s" % err)
def record_exclude_object_info(self, line):
import json
try:
if not os.path.exists(self.exclude_object_info):
with open(self.exclude_object_info, "w") as f:
data = {}
data["EXCLUDE_OBJECT_DEFINE"] = []
data["EXCLUDE_OBJECT"] = []
f.write(json.dumps(data))
f.flush()
with open(self.exclude_object_info, "r") as f:
ret = f.read()
if len(ret) > 0:
ret = eval(ret)
else:
ret = {}
if line.startswith("EXCLUDE_OBJECT_DEFINE"):
if line not in ret["EXCLUDE_OBJECT_DEFINE"]:
ret["EXCLUDE_OBJECT_DEFINE"].append(line)
elif line.startswith("EXCLUDE_OBJECT NAME"):
if line not in ret["EXCLUDE_OBJECT"]:
ret["EXCLUDE_OBJECT"].append(line)
with open(self.exclude_object_info, "w") as f:
f.write(json.dumps(ret))
f.flush()
except Exception as err:
logging.error("record_exclude_object_info error: %s" % err)
def run_script_from_command(self, script):
self._process_commands(script.split('\n'), need_ack=False)
def run_script(self, script):
with self.mutex:
self._process_commands(script.split('\n'), need_ack=False)
def get_mutex(self):
return self.mutex
def create_gcode_command(self, command, commandline, params):
return GCodeCommand(self, command, commandline, params, False)
# Response handling
def respond_raw(self, msg):
for cb in self.output_callbacks:
cb(msg)
def respond_info(self, msg, log=True):
if log:
logging.info(msg)
lines = [l.strip() for l in msg.strip().split('\n')]
self.respond_raw("// " + "\n// ".join(lines))
def _respond_error(self, msg):
from extras.tool import reportInformation
try:
v_sd = self.printer.lookup_object('virtual_sdcard')
if v_sd.print_id and "key" in msg and re.findall('key(\d+)', msg) and v_sd.cur_print_data:
v_sd.update_print_history_info(only_update_status=True, state="error", error_msg=eval(msg))
v_sd.print_id = ""
reportInformation("key701", data=v_sd.cur_print_data)
v_sd.cur_print_data = {}
except Exception as err:
logging.error(err)
try:
if "key" in msg and re.findall('key(\d+)', msg):
reportInformation(msg)
except Exception as err:
logging.error(err)
logging.warning(msg)
lines = msg.strip().split('\n')
if len(lines) > 1:
self.respond_info("\n".join(lines), log=False)
self.respond_raw('!! %s' % (lines[0].strip(),))
if self.is_fileinput:
self.printer.request_exit('error_exit')
def _respond_state(self, state):
self.respond_info("Klipper state: %s" % (state,), log=False)
# Parameter parsing helpers
extended_r = re.compile(
r'^\s*(?:N[0-9]+\s*)?'
r'(?P<cmd>[a-zA-Z_][a-zA-Z0-9_]+)(?:\s+|$)'
r'(?P<args>[^*;]*?)'
r'\s*(?:[#*;].*)?$')
def _get_extended_params(self, gcmd):
m = self.extended_r.match(gcmd.get_commandline())
if m is None:
raise self.error("""{"code":"key513", "msg": "Malformed command '%s'", "values": ["%s"]}""" % (gcmd.get_commandline(), gcmd.get_commandline()))
eargs = m.group('args')
try:
eparams = [earg.split('=', 1) for earg in shlex.split(eargs)]
eparams = { k.upper(): v for k, v in eparams }
gcmd._params.clear()
gcmd._params.update(eparams)
return gcmd
except ValueError as e:
raise self.error("""{"code":"key514", "msg": "Malformed command args '%s'", "values": ["%s"]}""" % (gcmd.get_commandline(), str(e)))
# G-Code special command handlers
def cmd_default(self, gcmd):
cmd = gcmd.get_command()
if cmd == 'M105':
# Don't warn about temperature requests when not ready
gcmd.ack("T:0")
return
if cmd == 'M21':
# Don't warn about sd card init when not ready
return
if not self.is_printer_ready:
raise gcmd.error(self.printer.get_state_message()[0])
return
if not cmd:
cmdline = gcmd.get_commandline()
if cmdline:
logging.debug(cmdline)
return
if cmd.startswith("M117 ") or cmd.startswith("M118 "):
# Handle M117/M118 gcode with numeric and special characters
handler = self.gcode_handlers.get(cmd[:4], None)
if handler is not None:
handler(gcmd)
return
elif cmd in ['M140', 'M104'] and not gcmd.get_float('S', 0.):
# Don't warn about requests to turn off heaters when not present
return
elif cmd == 'M107' or (cmd == 'M106' and (
not gcmd.get_float('S', 1.) or self.is_fileinput)):
# Don't warn about requests to turn off fan when fan not present
return
gcmd.respond_info("""{"code":"key61, "msg":"Unknown command:%s", "values": ["%s"]}""" % (cmd, cmd))
def get_muxcmd(self, cmdkey):
if cmdkey in self.mux_commands:
key, values = self.mux_commands[cmdkey]
return values
return None
def _cmd_mux(self, command, gcmd):
key, values = self.mux_commands[command]
if None in values:
key_param = gcmd.get(key, None)
else:
key_param = gcmd.get(key)
if key_param not in values:
raise gcmd.error("""{"code":"key69", "msg": "The value '%s' is not valid for %s", "values": ["%s", "%s"]}"""
% (key_param, key, key_param, key))
values[key_param](gcmd)
# Low-level G-Code commands that are needed before the config file is loaded
def cmd_M110(self, gcmd):
# Set Current Line Number
pass
def cmd_M112(self, gcmd):
# Emergency Stop
self.printer.invoke_shutdown("""{"code":"key70", "msg": "Shutdown due to M112 command", "values": []}""")
def cmd_M115(self, gcmd):
# Get Firmware Version and Capabilities
software_version = self.printer.get_start_args().get('software_version')
kw = {"FIRMWARE_NAME": "Klipper", "FIRMWARE_VERSION": software_version}
msg = " ".join(["%s:%s" % (k, v) for k, v in kw.items()])
did_ack = gcmd.ack(msg)
if not did_ack:
gcmd.respond_info(msg)
def request_restart(self, result):
if self.is_printer_ready:
toolhead = self.printer.lookup_object('toolhead')
print_time = toolhead.get_last_move_time()
if result == 'exit':
logging.info("Exiting (print time %.3fs)" % (print_time,))
self.printer.send_event("gcode:request_restart", print_time)
toolhead.dwell(0.500)
toolhead.wait_moves()
self.printer.request_exit(result)
cmd_RESTART_help = "Reload config file and restart host software"
def cmd_RESTART(self, gcmd):
self.request_restart('restart')
cmd_FIRMWARE_RESTART_help = "Restart firmware, host, and reload config"
def cmd_FIRMWARE_RESTART(self, gcmd):
self.request_restart('firmware_restart')
def cmd_ECHO(self, gcmd):
gcmd.respond_info(gcmd.get_commandline(), log=False)
cmd_STATUS_help = "Report the printer status"
def cmd_STATUS(self, gcmd):
if self.is_printer_ready:
self._respond_state("Ready")
return
msg = self.printer.get_state_message()[0]
msg = msg.rstrip() + "\nKlipper state: Not ready"
raise gcmd.error(msg)
cmd_HELP_help = "Report the list of available extended G-Code commands"
def cmd_HELP(self, gcmd):
cmdhelp = []
if not self.is_printer_ready:
cmdhelp.append("""{"code":"key72", "msg": "Printer is not ready - not all commands available.\n""")
cmdhelp.append("Available extended commands:")
for cmd in sorted(self.gcode_handlers):
if cmd in self.gcode_help:
cmdhelp.append("%-10s: %s" % (cmd, self.gcode_help[cmd]))
gcmd.respond_info("\n".join(cmdhelp), log=False)
# Support reading gcode from a pseudo-tty interface
class GCodeIO:
def __init__(self, printer):
self.printer = printer
printer.register_event_handler("klippy:ready", self._handle_ready)
printer.register_event_handler("klippy:shutdown", self._handle_shutdown)
self.gcode = printer.lookup_object('gcode')
self.gcode_mutex = self.gcode.get_mutex()
self.fd = printer.get_start_args().get("gcode_fd")
self.reactor = printer.get_reactor()
self.is_printer_ready = False
self.is_processing_data = False
self.is_fileinput = not not printer.get_start_args().get("debuginput")
self.pipe_is_active = True
self.fd_handle = None
if not self.is_fileinput:
self.gcode.register_output_handler(self._respond_raw)
self.fd_handle = self.reactor.register_fd(self.fd,
self._process_data)
self.partial_input = ""
self.pending_commands = []
self.bytes_read = 0
self.input_log = collections.deque([], 50)
def _handle_ready(self):
self.is_printer_ready = True
if self.is_fileinput and self.fd_handle is None:
self.fd_handle = self.reactor.register_fd(self.fd,
self._process_data)
def _dump_debug(self):
out = []
out.append("Dumping gcode input %d blocks" % (len(self.input_log),))
for eventtime, data in self.input_log:
out.append("Read %f: %s" % (eventtime, repr(data)))
logging.info("\n".join(out))
def _handle_shutdown(self):
if not self.is_printer_ready:
return
self.is_printer_ready = False
self._dump_debug()
if self.is_fileinput:
self.printer.request_exit('error_exit')
m112_r = re.compile('^(?:[nN][0-9]+)?\s*[mM]112(?:\s|$)')
def _process_data(self, eventtime):
# Read input, separate by newline, and add to pending_commands
try:
data = str(os.read(self.fd, 4096).decode())
except (os.error, UnicodeDecodeError):
logging.exception("Read g-code")
return
self.input_log.append((eventtime, data))
self.bytes_read += len(data)
lines = data.split('\n')
lines[0] = self.partial_input + lines[0]
self.partial_input = lines.pop()
pending_commands = self.pending_commands
pending_commands.extend(lines)
self.pipe_is_active = True
# Special handling for debug file input EOF
if not data and self.is_fileinput:
if not self.is_processing_data:
self.reactor.unregister_fd(self.fd_handle)
self.fd_handle = None
self.gcode.request_restart('exit')
pending_commands.append("")
# Handle case where multiple commands pending
if self.is_processing_data or len(pending_commands) > 1:
if len(pending_commands) < 20:
# Check for M112 out-of-order
for line in lines:
if self.m112_r.match(line) is not None:
self.gcode.cmd_M112(None)
if self.is_processing_data:
if len(pending_commands) >= 20:
# Stop reading input
self.reactor.unregister_fd(self.fd_handle)
self.fd_handle = None
return
# Process commands
self.is_processing_data = True
while pending_commands:
self.pending_commands = []
with self.gcode_mutex:
self.gcode._process_commands(pending_commands)
pending_commands = self.pending_commands
self.is_processing_data = False
if self.fd_handle is None:
self.fd_handle = self.reactor.register_fd(self.fd,
self._process_data)
def _respond_raw(self, msg):
if self.pipe_is_active:
try:
os.write(self.fd, (msg+"\n").encode())
# if 'key506' not in msg and 'key507' not in msg and 'key3"' not in msg and "key" in msg:
# reportInformation(msg)
except os.error:
logging.exception("Write g-code response")
self.pipe_is_active = False
def stats(self, eventtime):
return False, "gcodein=%d" % (self.bytes_read,)
def add_early_printer_objects(printer):
printer.add_object('gcode', GCodeDispatch(printer))
printer.add_object('gcode_io', GCodeIO(printer))

2
files/fixes/sudo Executable file
View file

@ -0,0 +1,2 @@
#!/bin/sh
exec $*

131
files/fixes/supervisorctl Executable file
View file

@ -0,0 +1,131 @@
#!/bin/sh
# supervisorctl shim - by destinal
# this is a fake supervisorctl that provides just enough information for moonraker to think it's the real thing.
# good enough to list the names of services in moonraker.conf, to say whether they're running or not (with false pids and times)
# and to start and stop them by name, finding and calling the matching init scripts.
# installing: put this in in /usr/bin/supervisorctl and then in moonraker.conf in [machine] section, set "provider: supervisord_cli"
if [ -t 1 ]; then # colorize only if we're on a terminal
GREEN='\033[32m'
RED='\033[31m'
ENDCOLOR='\033[0m'
fi
get_services() {
moonraker_pid="$(cat /var/run/moonraker.pid)"
# if moonraker is running, get its config directory from its command line
if [ -f /var/run/moonraker.pid ] && [ -d /proc/"$moonraker_pid" ] ; then
cmdline="$(tr '\0' '\n' < /proc/"$moonraker_pid"/cmdline)"
moonraker_dir="$(echo $cmdline | awk -F'-d ' '{print $2}' | awk '{print $1}')"
moonraker_conf="$moonraker_dir/config/moonraker.conf"
# services="klipper moonraker $(awk '/managed_services:/ {print $2}' $moonraker_conf | sed 's/://')"
# services=`(printf 'klipper\nmoonraker\n'; awk '/managed_services:/ {print $2}' $moonraker_conf | sed 's/://') | sort|uniq`
services=$(ls -1 /etc/init.d/S*|sed 's/.*\/S..//;s/_service$//')
echo $services
else
echo "Error: Invalid or missing PID file /var/run/moonraker.pid" >&2
exit 1
fi
}
get_pid_file() {
service="$1"
[ $service == "klipper" ] && service="klippy"
pid_file="/var/run/$service.pid"
echo $pid_file
}
is_running() {
service="$1"
pid_file="$(get_pid_file "$service")"
# Check for PID file
if [ -f "$pid_file" ] && [ -d "/proc/$(cat $pid_file)" ]; then
return 0 # Running
fi
# Fallback to using pidof in case the service doesn't use pid files
if pidof "$service" &>/dev/null; then
return 0 # Running
fi
return 1 # Not running
}
print_process_status() {
if is_running "$service"; then
printf "%-33s$GREEN""RUNNING$ENDCOLOR\n" "$service"
else
printf "%-33s$RED""STOPPED$ENDCOLOR\n" "$service"
fi
}
print_usage() {
echo "supervisorctl shim - provide minimal support for moonraker so CrealityOS moonraker can start/stop without systemd"
echo "Usage: $0 [command] <service>"
echo "commands include status stop start restart"
}
get_script_path() {
service="$1"
script_path="$(ls -1 /etc/init.d/S[0-9][0-9]${service}_service /etc/init.d/S[0-9][0-9]${service}* 2>/dev/null|head -1)"
echo "$script_path"
}
stop() {
service="$1"
script_path="$(get_script_path $service)"
# Check if the script exists and stop the service
if [[ -f "$script_path" ]]; then
"$script_path" stop
fi
}
start() {
service="$1"
script_path="$(get_script_path $service)"
# Check if the script exists and start the service
if [[ -f "$script_path" ]]; then
"$script_path" start
fi
}
restart() {
service="$1"
script_path="$(get_script_path $service)"
# Check if the script exists and restart the service
if [[ -f "$script_path" ]]; then
"$script_path" restart
fi
}
main() {
# echo "$0 $@" >> /tmp/supervisorctl.log
action="$1"; shift
case "$action" in
status)
if [ "$#" -lt 1 ]; then # just status, no arguments
for service in $(get_services); do
print_process_status $service
done
else
for service in "$@"; do # loop through the arguments provided
print_process_status $service
done
fi
;;
start)
start "$1"
;;
stop)
stop "$1"
;;
restart)
restart "$1"
;;
*)
print_usage
exit 1
esac
}
main "$@"

7
files/fixes/systemctl Executable file
View file

@ -0,0 +1,7 @@
#!/bin/sh
if [ "$1" == "reboot" ]; then
/sbin/reboot
elif [ "$1" == "poweroff" ]; then
/sbin/poweroff
fi

View file

@ -0,0 +1,167 @@
{
"blacklist": [
"fluidd.xyz",
"fluidd.net"
],
"endpoints": [
],
"hosted": false,
"themePresets": [
{
"name": "Fluidd",
"color": "#2196F3",
"isDark": true,
"logo": {
"src": "logo_fluidd.svg"
}
},
{
"name": "Annex",
"color": "#96CC4A",
"isDark": true,
"logo": {
"src": "logo_annex.svg"
}
},
{
"name": "BTT",
"color": "#475A91",
"isDark": true,
"logo": {
"src": "logo_btt.svg"
}
},
{
"name": "Creality V1",
"color": "#2196F3",
"isDark": true,
"logo": {
"src": "logo_creality_v1.svg"
}
},
{
"name": "Creality V2",
"color": "#2196F3",
"isDark": true,
"logo": {
"src": "logo_creality_v2.svg"
}
},
{
"name": "EVA",
"color": "#76FB00",
"isDark": true,
"logo": {
"src": "logo_eva.svg",
"dark": "#232323",
"light": "#ffffff"
}
},
{
"name": "HevORT",
"color": "#dfff3e",
"isDark": true,
"logo": {
"src": "logo_hevort.svg"
}
},
{
"name": "Kingroon",
"color": "#DA7A2C",
"isDark": true,
"logo": {
"src": "logo_kingroon.svg"
}
},
{
"name": "Klipper",
"color": "#B12F36",
"isDark": true,
"logo": {
"src": "logo_klipper.svg"
}
},
{
"name": "LDO",
"color": "#326799",
"isDark": true,
"logo": {
"src": "logo_ldo.svg"
}
},
{
"name": "Peopoly",
"color": "#007CC2",
"isDark": true,
"logo": {
"src": "logo_peopoly.svg"
}
},
{
"name": "Prusa",
"color": "#E05D2D",
"isDark": false,
"logo": {
"src": "logo_prusa.svg"
}
},
{
"name": "Qidi Tech",
"color": "#5B7AEA",
"isDark": true,
"logo": {
"src": "logo_qidi.svg"
}
},
{
"name": "RatRig",
"color": "#76FB00",
"isDark": true,
"logo": {
"src": "logo_ratrig.svg",
"dark": "#232323",
"light": "#ffffff"
}
},
{
"name": "Siboor",
"color": "#32E0DF",
"isDark": true,
"logo": {
"src": "logo_siboor.svg"
}
},
{
"name": "Voron",
"color": "#FF2300",
"isDark": true,
"logo": {
"src": "logo_voron.svg"
}
},
{
"name": "VzBot",
"color": "#FF2300",
"isDark": true,
"logo": {
"src": "logo_vzbot.svg"
}
},
{
"name": "ZeroG",
"color": "#e34234",
"isDark": true,
"logo": {
"src": "logo_zerog.svg"
}
},
{
"name": "SnakeOil",
"color": "#4bc3ca",
"isDark": true,
"logo": {
"src": "logo_snakeoil.svg"
}
}
]
}

View file

@ -0,0 +1,6 @@
<svg width="56" height="56" viewBox="0 0 56 56"
xmlns="http://www.w3.org/2000/svg">
<g>
<path fill="var(--v-primary-base, #2E75AE)" d="m 8.6551814,44.057554 c 0.3603498,-0.652878 1.2065683,-2.158274 1.8804856,-3.345324 0.673917,-1.18705 2.988293,-5.201439 5.143057,-8.920863 2.154764,-3.719425 5.310426,-9.1259 7.012583,-12.014389 1.702157,-2.888489 3.572196,-6.044964 4.155639,-7.014388 C 27.430391,11.793165 27.953897,11 28.010294,11 c 0.0564,0 1.099108,1.667266 2.317135,3.705036 1.218028,2.03777 3.477681,5.874101 5.021451,8.52518 1.543771,2.651079 4.050926,6.976716 5.571457,9.612526 1.520531,2.63581 3.698759,6.45218 4.840504,8.480822 1.141747,2.028642 2.075902,3.740774 2.075902,3.804739 v 0.116301 H 34.81516 21.793578 l 0.0037,-0.107914 c 0.002,-0.05935 0.803004,-1.5 1.779933,-3.201438 l 1.776235,-3.093526 5.618273,-0.03763 5.618275,-0.03763 -0.533989,-0.971457 C 35.76231,37.260709 33.947866,34.068448 32.023906,30.7011 30.099946,27.33375 28.41152,24.427148 28.27185,24.241982 l -0.253947,-0.336665 -1.364829,2.28835 c -0.750656,1.258591 -3.146114,5.363888 -5.323238,9.122881 -2.177125,3.758992 -4.369345,7.530575 -4.871603,8.381295 l -0.913195,1.546762 H 11.772517 8 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,122 @@
<svg
id="Layer_1"
data-name="Layer 1"
width="56"
height="56"
viewBox="0 0 56 56"
version="1.1"
sodipodi:docname="logo_creality.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview240"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false" />
<defs
id="defs227">
<style
id="style182">
.cls-1 {
fill: url(#linear-gradient-3);
}
.cls-1, .cls-2 {
fill-rule: evenodd;
}
.cls-1, .cls-2, .cls-3, .cls-4 {
stroke-width: 0px;
}
.cls-2 {
fill: url(#linear-gradient);
}
.cls-3 {
fill: url(#linear-gradient-2);
}
.cls-4 {
fill: #fff;
opacity: 0;
}
</style>
<linearGradient
id="linear-gradient-2"
x1="28"
y1="44.608002"
x2="28.002001"
y2="34.222"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#8bb034"
id="stop201"
style="stop-color:var(--v-primary-base, #2196F3);stop-opacity:1;" />
<stop
offset="0.20200001"
stop-color="#87ad33"
id="stop203"
style="stop-color:var(--v-primary-darken2, #2E75AE);stop-opacity:1;" />
<stop
offset="0.38100001"
stop-color="#7ca533"
id="stop205"
style="stop-color:var(--v-primary-darken2, #2E75AE);stop-opacity:1;" />
<stop
offset="0.55199999"
stop-color="#6a9832"
id="stop207"
style="stop-color:var(--v-primary-darken2, #2E75AE);stop-opacity:1;" />
<stop
offset="0.71700001"
stop-color="#508632"
id="stop209"
style="stop-color:var(--v-primary-darken2, #2E75AE);stop-opacity:1;" />
<stop
offset="0.87800002"
stop-color="#306e31"
id="stop211"
style="stop-color:var(--v-primary-darken2, #2E75AE);stop-opacity:1;" />
<stop
offset="1"
stop-color="#125930"
id="stop213"
style="stop-color:var(--v-primary-darken2, #2E75AE);stop-opacity:1;" />
</linearGradient>
</defs>
<rect
class="cls-4"
width="56"
height="56"
id="rect229"
x="0"
y="0" />
<g
id="g237">
<path
class="cls-2"
d="m 47.869,43.265 c -0.035,-0.08 -1.131,-2.107 -2.435,-4.504 -3.689,-6.778 -11.028,-20.27 -13.546,-24.902 -0.539,-0.993 -1.026,-1.882 -1.082,-1.975 -0.607,-1.028 -1.681,-1.647 -2.832,-1.631 -0.108,10e-4 -0.222,0.006 -0.252,0.009 -1.04,0.121 -1.875,0.63 -2.433,1.484 -0.064,0.098 -0.535,0.948 -1.046,1.889 -2.339,4.303 -7.668,14.1 -13.713,25.211 -1.307,2.402 -2.397,4.428 -2.424,4.501 -0.159,0.444 -0.136,0.945 0.061,1.381 0.121,0.266 0.368,0.564 0.596,0.718 0.284,0.191 0.559,0.281 0.911,0.295 0.407,0.017 0.119,0.124 4.694,-1.748 8.877,-3.633 10.629,-4.348 10.846,-4.426 0.481,-0.174 1.13,-0.33 1.667,-0.401 1.357,-0.18 2.766,-0.027 4.007,0.435 0.12,0.045 3.509,1.428 7.531,3.074 4.022,1.646 7.372,3.008 7.444,3.027 0.102,0.027 0.194,0.035 0.421,0.034 0.26,0 0.307,-0.006 0.457,-0.052 0.212,-0.065 0.453,-0.19 0.608,-0.316 0.258,-0.21 0.491,-0.566 0.592,-0.903 0.04,-0.135 0.052,-0.217 0.059,-0.429 0.011,-0.311 -0.027,-0.536 -0.13,-0.771 z m -11.721,-8.81 c -0.054,0.261 -0.202,0.455 -0.427,0.562 -0.163,0.077 -0.37,0.092 -0.529,0.038 -0.057,-0.019 -1.097,-0.601 -2.312,-1.292 -1.215,-0.692 -2.296,-1.303 -2.403,-1.358 -0.542,-0.281 -1.119,-0.473 -1.699,-0.565 -0.261,-0.041 -1.011,-0.057 -1.304,-0.028 -0.455,0.045 -1.002,0.18 -1.407,0.346 -0.38,0.156 -0.718,0.34 -2.906,1.585 -1.214,0.691 -2.258,1.277 -2.319,1.301 -0.153,0.062 -0.38,0.054 -0.54,-0.02 -0.354,-0.162 -0.542,-0.592 -0.416,-0.949 0.013,-0.036 1.467,-3.028 3.231,-6.649 1.764,-3.621 3.332,-6.84 3.483,-7.154 0.312,-0.644 0.39,-0.774 0.563,-0.933 0.291,-0.266 0.676,-0.381 1.055,-0.313 0.277,0.05 0.503,0.175 0.705,0.392 0.145,0.155 0.167,0.196 0.614,1.116 0.202,0.417 1.768,3.632 3.479,7.144 3.433,7.046 3.187,6.515 3.134,6.775 z"
id="path231"
style="fill:var(--v-primary-base, #2196F3);fill-opacity:1" />
<path
class="cls-3"
d="m 26.851,34.093 -5.479,3.228 c -2.105,1.263 -4.038,2.423 -4.296,2.578 -0.257,0.155 -0.809,0.487 -1.225,0.738 -1.668,1.002 -4.248,2.555 -4.442,2.673 -3.0029708,1.704833 -2.7606933,1.506138 -3.0176933,1.660138 l 0.051014,0.08915 0.6450702,0.412958 L 10.114155,45.63983 15.85,43.381 c 0.891,-0.343 2.755,-1.06 4.144,-1.594 1.389,-0.534 3.08,-1.185 3.759,-1.446 1.356,-0.522 1.514,-0.579 1.927,-0.69 0.468,-0.127 0.909,-0.208 1.439,-0.267 0.395,-0.044 1.362,-0.044 1.758,0 0.646,0.071 1.205,0.186 1.767,0.363 0.18,0.057 0.912,0.328 1.627,0.604 0.715,0.275 2.407,0.926 3.759,1.446 1.353,0.52 3.197,1.229 4.099,1.576 l 5.565998,2.185041 1.108556,-0.004 0.78139,-0.49475 L 45.121,43.629 c -0.336,-0.202 -1.023,-0.616 -1.527,-0.92 -0.966,-0.583 -1.581,-0.953 -2.852,-1.716 -0.427,-0.256 -1.05,-0.631 -1.384,-0.832 -0.656,-0.395 -8.261,-5.054 -8.4,-5.137 0,0 -1.24,-0.737 -1.966,-1.154 -0.239,-0.137 -1.055,-0.526 -2.142,0.222 z"
id="path233"
sodipodi:nodetypes="ccccccccccsccccccccccccccccccc"
style="fill:url(#linear-gradient-2)" />
<path
class="cls-1"
d="m 33.207,25.075 c -1.779,-4.886 -3.406,-9.357 -3.616,-9.937 -0.464,-1.279 -0.487,-1.337 -0.638,-1.552 -0.21,-0.301 -0.445,-0.475 -0.733,-0.545 -0.394,-0.095 -0.794,0.064 -1.096,0.435 -0.18,0.22 -0.261,0.401 -0.585,1.297 -0.158,0.436 -1.787,4.914 -3.621,9.95 -1.834,5.036 -3.345,9.197 -3.358,9.248 -0.131,0.496 0.065,1.094 0.433,1.32 0.166,0.102 0.71,0.171 0.87,0.085 0.052,-0.028 3.669,-2.34 5.301,-3.257 -0.034,0.013 -0.069,0.026 -0.102,0.039 -0.38,0.156 -0.718,0.34 -2.906,1.585 -1.214,0.691 -2.258,1.277 -2.319,1.301 -0.153,0.062 -0.38,0.054 -0.54,-0.02 -0.354,-0.162 -0.542,-0.592 -0.416,-0.949 0.013,-0.036 1.467,-3.028 3.231,-6.649 1.764,-3.621 3.332,-6.84 3.483,-7.154 0.312,-0.644 0.39,-0.774 0.563,-0.933 0.291,-0.266 0.676,-0.381 1.055,-0.313 0.277,0.05 0.503,0.175 0.705,0.392 0.145,0.155 0.167,0.196 0.614,1.116 0.202,0.417 1.768,3.632 3.479,7.144 3.433,7.046 3.187,6.515 3.134,6.775 -0.054,0.261 -0.202,0.455 -0.427,0.562 -0.163,0.077 -0.37,0.092 -0.529,0.038 -0.057,-0.019 -1.097,-0.601 -2.312,-1.292 -1.215,-0.692 -2.296,-1.303 -2.403,-1.358 -0.158,-0.082 -0.319,-0.155 -0.481,-0.221 1.638,0.944 5.056,3.211 5.103,3.232 0.165,0.075 0.758,-0.029 0.927,-0.137 0.234,-0.148 0.388,-0.419 0.444,-0.781 0.056,-0.361 0.311,0.377 -3.257,-9.423 z"
id="path235"
style="fill:var(--v-primary-darken2, #2E75AE);fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -0,0 +1,87 @@
# Run a shell command via gcode
#
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import os
import shlex
import subprocess
import logging
class ShellCommand:
def __init__(self, config):
self.name = config.get_name().split()[-1]
self.printer = config.get_printer()
self.gcode = self.printer.lookup_object('gcode')
cmd = config.get('command')
cmd = os.path.expanduser(cmd)
self.command = shlex.split(cmd)
self.timeout = config.getfloat('timeout', 2., above=0.)
self.verbose = config.getboolean('verbose', True)
self.proc_fd = None
self.partial_output = ""
self.gcode.register_mux_command(
"RUN_SHELL_COMMAND", "CMD", self.name,
self.cmd_RUN_SHELL_COMMAND,
desc=self.cmd_RUN_SHELL_COMMAND_help)
def _process_output(self, eventime):
if self.proc_fd is None:
return
try:
data = os.read(self.proc_fd, 4096)
except Exception:
pass
data = self.partial_output + data.decode()
if '\n' not in data:
self.partial_output = data
return
elif data[-1] != '\n':
split = data.rfind('\n') + 1
self.partial_output = data[split:]
data = data[:split]
else:
self.partial_output = ""
self.gcode.respond_info(data)
cmd_RUN_SHELL_COMMAND_help = "Run a linux shell command"
def cmd_RUN_SHELL_COMMAND(self, params):
gcode_params = params.get('PARAMS','')
gcode_params = shlex.split(gcode_params)
reactor = self.printer.get_reactor()
try:
proc = subprocess.Popen(
self.command + gcode_params, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except Exception:
logging.exception(
"shell_command: Command {%s} failed" % (self.name))
raise self.gcode.error("Error running command {%s}" % (self.name))
if self.verbose:
self.proc_fd = proc.stdout.fileno()
self.gcode.respond_info("Running Command {%s}...:" % (self.name))
hdl = reactor.register_fd(self.proc_fd, self._process_output)
eventtime = reactor.monotonic()
endtime = eventtime + self.timeout
complete = False
while eventtime < endtime:
eventtime = reactor.pause(eventtime + .05)
if proc.poll() is not None:
complete = True
break
if not complete:
proc.terminate()
if self.verbose:
if self.partial_output:
self.gcode.respond_info(self.partial_output)
self.partial_output = ""
if complete:
msg = "Command {%s} finished\n" % (self.name)
else:
msg = "Command {%s} timed out" % (self.name)
self.gcode.respond_info(msg)
reactor.unregister_fd(hdl)
self.proc_fd = None
def load_config_prefix(config):
return ShellCommand(config)

25
files/git-backup/S52Git-Backup Executable file
View file

@ -0,0 +1,25 @@
#!/bin/sh
case "$1" in
start)
echo "Starting Git Backup..."
/usr/data/helper-script/files/git-backup/git-backup.sh -b "$BRANCH" -t "$IFS" -g origin & > /dev/null
;;
stop)
echo "Stopping Git Backup..."
pkill Git-Backup
pkill inotifywait
;;
restart)
echo "Restarting Git Backup..."
pkill Git-Backup
pkill inotifywait
sleep 1
/usr/data/helper-script/files/git-backup/git-backup.sh -b "$BRANCH" -t "$IFS" -g origin & > /dev/null
;;
*)
Usage: $0 {start|stop|restart}
exit 1
;;
esac
exit 0

View file

@ -0,0 +1,35 @@
########################################
# Git Backup
########################################
[gcode_shell_command Backup_Stop]
command: sh /usr/data/helper-script/files/git-backup/git-backup.sh -s
timeout: 600.0
verbose: true
[gcode_shell_command Backup_Pause]
command: sh /usr/data/helper-script/files/git-backup/git-backup.sh -p
timeout: 600.0
verbose: true
[gcode_shell_command Backup_Resume]
command: sh /usr/data/helper-script/files/git-backup/git-backup.sh -s
timeout: 600.0
verbose: true
[gcode_macro GIT_BACKUP_STOP]
gcode:
RUN_SHELL_COMMAND CMD=Backup_Stop
[gcode_macro GIT_BACKUP_PAUSE]
gcode:
RUN_SHELL_COMMAND CMD=Backup_Pause
[gcode_macro GIT_BACKUP_RESUME]
gcode:
RUN_SHELL_COMMAND CMD=Backup_Resume

310
files/git-backup/git-backup.sh Executable file
View file

@ -0,0 +1,310 @@
#!/bin/sh
#
# This program is based off of gitwatch @ https://github.com/gitwatch/gitwatch.git
# Copyright (C) 2013-2018 Patrick Lehner
# with modifications and contributions by:
# - Matthew McGowan
# - Dominik D. Geyer
# - Phil Thompson
# - Dave Musicant
#
# Edited to work on busybox ash shell, specifically the Creality K1 & K1Max
#############################################################################
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#############################################################################
#
# Idea and original code taken from http://stackoverflow.com/a/965274
# original work by Lester Buck
# (but heavily modified by now)
#
# Requires the command 'inotifywait' to be available, which is part of
# the inotify-tools (See https://github.com/rvoicilas/inotify-tools ),
# and (obviously) git.
# Will check the availability of both commands using the `which` command
# and will abort if either command (or `which`) is not found.
#
white=`echo -en "\033[m"`
yellow=`echo -en "\033[1;33m"`
green=`echo -en "\033[01;32m"`
INSTALL=0
PAUSE=0
RESUME=0
STOP=0
REMOTE=""
BRANCH=""
TARGET=""
EVENTS="${EVENTS:-close_write,move,move_self,delete,create,modify}"
SLEEP_TIME=5
DATE_FMT="+%d-%m-%Y (%H:%M:%S)"
COMMITMSG="Auto-commit on %d by Git Backup"
SKIP_IF_MERGING=0
# Function to print script help
shelp() {
echo "Usage: $(basename "$0") [-i] [-p] [-r] [-s] -b branch -t target -g remote"
echo "Options:"
echo " -i Install"
echo " -p Pause"
echo " -r Resume"
echo " -s Stop"
echo " -b branch Specify branch for git push"
echo " -t target Specify target directory or file to watch"
echo " -g remote Specify remote for git push"
}
# Parse command-line arguments
while getopts "iprsb:t:g:hn" option; do
case "${option}" in
i) INSTALL=1 ;;
p) PAUSE=1 ;;
r) RESUME=1 ;;
s) STOP=1 ;;
b) BRANCH="${OPTARG}" ;;
t) TARGET="${OPTARG}" ;;
g) REMOTE="${OPTARG}" ;;
h)
shelp
exit 0
;;
\?)
echo "Invalid option: -$OPTARG" >&2
shelp
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
shelp
exit 1
;;
*)
shelp
exit 0
;;
esac
done
# Check if more than one flag is used
if [ "$((INSTALL + PAUSE + RESUME + STOP))" -gt 1 ]; then
echo "Error: Only one flag is allowed at a time."
shelp
exit 1
fi
# Pause, Resume, Stop flags
if [ "$PAUSE" = 1 ]; then
echo "Info: Pausing automatic backups until the next reboot or manually restarted..."
/etc/init.d/S52Git-Backup stop
exit 0
elif [ "$STOP" = 1 ]; then
echo "Info: Stopping automatic backups until manually restarted..."
mv /etc/init.d/S52Git-Backup /etc/init.d/disabled.S52Git-Backup
exit 0
elif [ "$RESUME" = 1 ]; then
echo "Info: Resuming automatic backups..."
mv /etc/init.d/disabled.S52Git-Backup /etc/init.d/S52Git-Backup
exit 0
elif [ "$INSTALL" = 1 ]; then
# Install required packages using opkg
if [ -f /opt/bin/opkg ]; then
/opt/bin/opkg update
/opt/bin/opkg install inotifywait procps-ng-pkill
else
echo "Error: opkg package manager not found. Please install Entware."
exit 1
fi
# Prompt user for configuration
echo "${white}"
read -p " Please enter your ${green}GitHub username${white} and press Enter: ${yellow}" USER_NAME
echo "${white}"
read -p " Please enter your ${green}GitHub repository name${white} and press Enter: ${yellow}" REPO_NAME
echo "${white}"
read -p " Please enter your ${green}GitHub personal access token${white} and press Enter: ${yellow}" GITHUB_TOKEN
echo "${white}"
# Prompt user to select folders to be watched
IFS=/usr/data/printer_data/config
# Connect config directory to github
cd "$IFS" || exit
git init
git remote add origin "https://$USER_NAME:$GITHUB_TOKEN@github.com/$USER_NAME/$REPO_NAME.git"
git checkout -b "$BRANCH"
git add .
git commit -m "Initial Backup"
git push -u origin "$BRANCH"
# Write configuration to .env file
echo "IFS=$IFS" > "$IFS/.env"
echo "GITHUB_TOKEN=$GITHUB_TOKEN" >> "$IFS/.env"
echo "REMOTE=$REPO_NAME" >> "$IFS/.env"
echo "BRANCH=$BRANCH" >> "$IFS/.env"
echo "USER=$USER_NAME" >> "$IFS/.env"
# Create .gitignore file to protect .env variables
echo ".env" > "$IFS/.gitignore"
# Insert .env to S52gitwatch.sh and move to init.d
cp -f /usr/data/helper-script/files/git-backup/S52Git-Backup /etc/init.d/S52Git-Backup
sed -i "2i source $IFS/.env" /etc/init.d/S52Git-Backup
chmod +x /etc/init.d/S52Git-Backup
/etc/init.d/S52Git-Backup start
exit 0
fi
# print all arguments to stderr
stderr() {
echo "$@" >&2
}
# clean up at end of program, killing the remaining sleep process if it still exists
cleanup() {
if [ -n "$SLEEP_PID" ] && kill -0 "$SLEEP_PID" 2>/dev/null; then
kill "$SLEEP_PID" 2>/dev/null
fi
exit 0
}
# Tests for the availability of a command
is_command() {
command -v "$1" >/dev/null 2>&1
}
# Test whether or not current git directory has ongoing merge
is_merging () {
[ -f "$(git rev-parse --git-dir)"/MERGE_HEAD ]
}
shift $((OPTIND - 1)) # Shift the input arguments, so that the input file (last arg) is $1 in the code below
GIT="git"
RL="readlink"
INW="inotifywait"
# Check availability of selected binaries and die if not met
for cmd in "$GIT" "$INW"; do
is_command "$cmd" || {
stderr "Error: Required command '$cmd' not found."
exit 2
}
done
SLEEP_PID="" # pid of timeout subprocess
trap "cleanup" EXIT # make sure the timeout is killed when exiting script
# Expand the path to the target to absolute path
if [ "$(uname)" != "Darwin" ]; then
IN=$($RL -f "$TARGET")
else
if is_command "greadlink"; then
IN=$(greadlink -f "$TARGET")
else
IN=$($RL -f "$TARGET")
if [ $? -eq 1 ]; then
echo "Info: Seems like your readlink doesn't support '-f'. Running without. Please 'brew install coreutils'."
IN=$($RL "$TARGET")
fi
fi
fi
if [ -d "$TARGET" ]; then # if the target is a directory
TARGETDIR=$(echo "$IN" | sed -e "s/\/*$//") # dir to CD into before using git commands: trim trailing slash, if any
# construct inotifywait-commandline
if [ "$(uname)" != "Darwin" ]; then
INW_ARGS="-qmr -e $EVENTS $TARGETDIR"
fi
GIT_ADD="git add -A ." # add "." (CWD) recursively to index
GIT_COMMIT_ARGS="-a" # add -a switch to "commit" call just to be sure
else
stderr "Error: The target is neither a regular file nor a directory."
exit 3
fi
# CD into the right dir
cd "$TARGETDIR" || {
stderr "Error: Can't change directory to '${TARGETDIR}'."
exit 5
}
if [ -n "$REMOTE" ]; then # are we pushing to a remote?
if [ -z "$BRANCH" ]; then # Do we have a branch set to push to ?
PUSH_CMD="$GIT push $REMOTE" # Branch not set, push to remote without a branch
else
# check if we are on a detached HEAD
if HEADREF=$($GIT symbolic-ref HEAD 2> /dev/null); then # HEAD is not detached
PUSH_CMD="$GIT push $REMOTE ${HEADREF#refs/heads/}:$BRANCH"
else # HEAD is detached
PUSH_CMD="$GIT push $REMOTE $BRANCH"
fi
fi
else
PUSH_CMD="" # if no remote is selected, make sure the push command is empty
fi
# main program loop: wait for changes and commit them
# whenever inotifywait reports a change, we spawn a timer (sleep process) that gives the writing
# process some time (in case there are a lot of changes or w/e); if there is already a timer
# running when we receive an event, we kill it and start a new one; thus we only commit if there
# have been no changes reported during a whole timeout period
# Custom timeout function
# main program loop: wait for changes and commit them
# Custom timeout function
timeout() {
sleep "5" &
timeout_pid=$!
trap "kill $timeout_pid 2>/dev/null" EXIT
wait $timeout_pid 2>/dev/null
}
while true; do
# Start inotifywait to monitor changes
eval "$INW $INW_ARGS" | while read -r line; do
# Check if there were any changes reported during the timeout period
if [ -n "$line" ]; then
# Process changes
if [ -n "$DATE_FMT" ]; then
COMMITMSG=$(echo "$COMMITMSG" | awk -v date="$(date "$DATE_FMT")" '{gsub(/%d/, date)}1') # splice the formatted date-time into the commit message
fi
cd "$TARGETDIR" || {
stderr "Error: Can't change directory to '${TARGETDIR}'."
exit 6
}
STATUS=$($GIT status -s)
if [ -n "$STATUS" ]; then # only commit if status shows tracked changes.
if [ "$SKIP_IF_MERGING" -eq 1 ] && is_merging; then
echo "Skipping commit - repo is merging"
continue
fi
$GIT_ADD # add file(s) to index
$GIT commit $GIT_COMMIT_ARGS -m "$COMMITMSG" # construct commit message and commit
if [ -n "$PUSH_CMD" ]; then
echo "Push command is $PUSH_CMD"
eval "$PUSH_CMD"
pkill 'inotifywait'
timeout
fi
fi
fi
done
done

View file

@ -0,0 +1,44 @@
#!/bin/sh
GUPPY_DIR="/usr/data/guppyscreen"
CURL="/usr/data/helper-script/files/fixes/curl"
VERSION_FILE="$GUPPY_DIR/.version"
CUSTOM_UPGRADE_SCRIPT="$GUPPY_DIR/custom_upgrade.sh"
if [ -f "$VERSION_FILE" ]; then
CURRENT_VERSION=$(jq -r '.version' "$VERSION_FILE")
THEME=$(jq -r '.theme' "$VERSION_FILE")
ASSET_NAME=$(jq '.asset_name' "$VERSION_FILE")
fi
"$CURL" -s https://api.github.com/repos/ballaswag/guppyscreen/releases -o /tmp/guppy-releases.json
latest_version=$(jq -r '.[0].tag_name' /tmp/guppy-releases.json)
if [ "$(printf '%s\n' "$CURRENT_VERSION" "$latest_version" | sort -V | head -n1)" = "$latest_version" ]; then
echo "Guppy Screen $CURRENT_VERSION is already up to date!"
rm -f /tmp/guppy-releases.json
exit 0
else
asset_url=$(jq -r ".[0].assets[] | select(.name == $ASSET_NAME).browser_download_url" /tmp/guppy-releases.json)
echo "Downloading latest version $latest_version from $asset_url"
"$CURL" -L "$asset_url" -o /usr/data/guppyscreen.tar.gz
fi
tar -xvf /usr/data/guppyscreen.tar.gz -C "$GUPPY_DIR/.."
if [ -f "$CUSTOM_UPGRADE_SCRIPT" ]; then
echo "Running custom_upgrade.sh for release $latest_version..."
"$CUSTOM_UPGRADE_SCRIPT"
fi
echo "Guppy Screen have been updated to version $latest_version!"
if grep -Fqs "ID=buildroot" /etc/os-release
then
[ -f /etc/init.d/S99guppyscreen ] && /etc/init.d/S99guppyscreen stop &> /dev/null
killall -q guppyscreen
/etc/init.d/S99guppyscreen restart &> /dev/null
rm -f /usr/data/guppyscreen.tar.gz
rm -f /tmp/guppy-releases.json
fi
exit 0

View file

@ -0,0 +1,37 @@
########################################
# Guppy Screen Update
########################################
[gcode_shell_command guppy_update]
command: sh /usr/data/helper-script/files/guppy-screen/guppy-update.sh
timeout: 600.0
verbose: True
[gcode_macro GUPPY_UPDATE]
description: Check for Guppy Screen Updates
gcode:
{% if printer.idle_timeout.state == "Printing" %}
RESPOND TYPE=error MSG="It's not possible to update Guppy Screen while printing!"
{% else %}
RUN_SHELL_COMMAND CMD=guppy_update
{% endif %}
[gcode_macro INPUT_SHAPER_CALIBRATION]
description: Measure X and Y Axis Resonances and Save values
gcode:
{% if printer["configfile"].config["temperature_fan mcu_fan"] %}
SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=30
{% endif %}
{% if printer.toolhead.homed_axes != "xyz" %}
RESPOND TYPE=command MSG="Homing..."
G28
{% endif %}
RESPOND TYPE=command MSG="Measuring X and Y Resonances..."
SHAPER_CALIBRATE
M400
{% if printer["configfile"].config["temperature_fan mcu_fan"] %}
SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=50
{% endif %}
CXSAVE_CONFIG

View file

@ -0,0 +1,39 @@
class CalibrateShaperConfig:
def __init__(self, config):
self.printer = config.get_printer();
shaper_type = config.get('shaper_type', 'mzv')
self.shaper_type_x = config.get('shaper_type_x' , shaper_type)
self.shaper_freq_x = config.getfloat('shaper_freq_x', 0., minval=0.)
self.shaper_type_y = config.get('shaper_type_y' , shaper_type)
self.shaper_freq_y = config.getfloat('shaper_freq_y', 0., minval=0.)
# Register commands
gcode = config.get_printer().lookup_object('gcode')
gcode.register_command("SAVE_INPUT_SHAPER", self.cmd_save_input_shaper)
def get_status(self, eventtime):
return {}
def cmd_save_input_shaper(self, gcmd):
self.shaper_freq_x = gcmd.get_float('SHAPER_FREQ_X',
self.shaper_freq_x, minval=0.)
self.shaper_type_x = gcmd.get('SHAPER_TYPE_X', self.shaper_type_x)
self.shaper_freq_y = gcmd.get_float('SHAPER_FREQ_Y',
self.shaper_freq_y, minval=0.)
self.shaper_type_y = gcmd.get('SHAPER_TYPE_Y', self.shaper_type_y)
configfile = self.printer.lookup_object('configfile')
configfile.set('input_shaper', 'shaper_type_x', self.shaper_type_x)
configfile.set('input_shaper', 'shaper_freq_x',
'%.1f' % (self.shaper_freq_x,))
configfile.set('input_shaper', 'shaper_type_y', self.shaper_type_y)
configfile.set('input_shaper', 'shaper_freq_y',
'%.1f' % (self.shaper_freq_y,))
def load_config(config):
return CalibrateShaperConfig(config)

View file

@ -0,0 +1,118 @@
########################################
# Improved Shapers Configurations
########################################
[respond]
[calibrate_shaper_config]
[gcode_shell_command resonance_graph]
command: /usr/data/printer_data/config/Helper-Script/improved-shapers/scripts/calibrate_shaper.py
timeout: 600.0
verbose: True
[gcode_shell_command belts_graph]
command: /usr/data/printer_data/config/Helper-Script/improved-shapers/scripts/graph_belts.py
timeout: 600.0
verbose: True
[gcode_macro INPUT_SHAPER_CALIBRATION]
description: Measure X and Y Axis Resonances and Save values
gcode:
{% if printer["configfile"].config["temperature_fan mcu_fan"] %}
SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=30
{% endif %}
{% if printer.toolhead.homed_axes != "xyz" %}
RESPOND TYPE=command MSG="Homing..."
G28
{% endif %}
RESPOND TYPE=command MSG="Measuring X and Y Resonances..."
SHAPER_CALIBRATE
M400
{% if printer["configfile"].config["temperature_fan mcu_fan"] %}
SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=50
{% endif %}
CXSAVE_CONFIG
[gcode_macro TEST_RESONANCES_GRAPHS]
description: Test X and Y Axis Resonances and Generate Graphs
gcode:
{% set x_png = params.X_PNG|default("/usr/data/printer_data/config/Helper-Script/improved-shapers/resonances_x.png") %}
{% set y_png = params.Y_PNG|default("/usr/data/printer_data/config/Helper-Script/improved-shapers/resonances_y.png") %}
{% if printer["configfile"].config["temperature_fan mcu_fan"] %}
SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=30
{% endif %}
{% if printer.toolhead.homed_axes != "xyz" %}
RESPOND TYPE=command MSG="Homing..."
G28
{% endif %}
RESPOND TYPE=command MSG="Testing X Resonances..."
TEST_RESONANCES AXIS=X NAME=x
M400
RESPOND TYPE=command MSG="Generating X Graphs... This may take some time."
RUN_SHELL_COMMAND CMD=resonance_graph PARAMS="/tmp/resonances_x_x.csv -o {x_png}"
RESPOND TYPE=command MSG="X Graph (resonances_x.png) is available in /Helper-Script/improved-shapers folder."
RESPOND TYPE=command MSG="Testing Y Resonances..."
TEST_RESONANCES AXIS=Y NAME=y
M400
RESPOND TYPE=command MSG="Generating Y Graphs... This may take some time."
RUN_SHELL_COMMAND CMD=resonance_graph PARAMS="/tmp/resonances_y_y.csv -o {y_png}"
RESPOND TYPE=command MSG="Y Graph (resonances_y.png) is available in /Helper-Script/improved-shapers folder."
{% if printer["configfile"].config["temperature_fan mcu_fan"] %}
SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=50
{% endif %}
[gcode_macro BELTS_SHAPER_CALIBRATION]
description: Perform a custom half-axis test to analyze and compare the frequency profiles of individual belts on CoreXY printers
gcode:
{% set min_freq = params.FREQ_START|default(5)|float %}
{% set max_freq = params.FREQ_END|default(133.33)|float %}
{% set hz_per_sec = params.HZ_PER_SEC|default(1)|float %}
{% set png_width = params.PNG_WIDTH|default(8)|float %}
{% set png_height = params.PNG_HEIGHT|default(4.8)|float %}
{% set png_out_path = params.PNG_OUT_PATH|default("/usr/data/printer_data/config/Helper-Script/improved-shapers/belts_calibration.png") %}
{% if printer["configfile"].config["temperature_fan mcu_fan"] %}
SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=30
{% endif %}
{% if printer.toolhead.homed_axes != "xyz" %}
RESPOND TYPE=command MSG="Homing..."
G28
{% endif %}
TEST_RESONANCES AXIS=1,1 OUTPUT=raw_data NAME=b FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec}
M400
TEST_RESONANCES AXIS=1,-1 OUTPUT=raw_data NAME=a FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec}
M400
RESPOND TYPE=command MSG="Generating belts comparative frequency profile..."
RESPOND TYPE=command MSG="This may take some time (3-5min)."
RUN_SHELL_COMMAND CMD=belts_graph PARAMS="-w {png_width} -l {png_height} -n -o {png_out_path} -k /usr/share/klipper /tmp/raw_data_axis=1.000,-1.000_a.csv /tmp/raw_data_axis=1.000,1.000_b.csv"
RESPOND TYPE=command MSG="Graph (belts_calibration.png) is available in /Helper-Script/improved-shapers folder."
{% if printer["configfile"].config["temperature_fan mcu_fan"] %}
SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=mcu_fan TARGET=50
{% endif %}
[gcode_macro EXCITATE_AXIS_AT_FREQ]
description: Maintain a specified excitation frequency for a period of time to diagnose and locate a vibration source
gcode:
{% set frequency = params.FREQUENCY|default(25)|int %}
{% set time = params.TIME|default(10)|int %}
{% set axis = params.AXIS|default("x")|string|lower %}
{% if axis not in ["x", "y", "a", "b"] %}
{ action_raise_error("AXIS selection is invalid. Should be either x, y, a or b!") }
{% endif %}
{% if axis == "a" %}
{% set axis = "1,-1" %}
{% elif axis == "b" %}
{% set axis = "1,1" %}
{% endif %}
{% if printer.toolhead.homed_axes != "xyz" %}
RESPOND TYPE=command MSG="Homing..."
G28
{% endif %}
TEST_RESONANCES OUTPUT=raw_data AXIS={axis} FREQ_START={frequency-1} FREQ_END={frequency+1} HZ_PER_SEC={1/(time/3)}
M400

View file

@ -0,0 +1,186 @@
#!/usr/bin/env python3
###!/usr/data/rootfs/usr/bin/python3
# Shaper auto-calibration script
#
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from __future__ import print_function
import importlib, optparse, os, sys, pathlib
from textwrap import wrap
import numpy as np, matplotlib
import shaper_calibrate
import json
MAX_TITLE_LENGTH=65
def parse_log(logname):
with open(logname) as f:
for header in f:
if not header.startswith('#'):
break
if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
# Raw accelerometer data
return np.loadtxt(logname, comments='#', delimiter=',')
# Parse power spectral density data
data = np.loadtxt(logname, skiprows=1, comments='#', delimiter=',')
calibration_data = shaper_calibrate.CalibrationData(
freq_bins=data[:,0], psd_sum=data[:,4],
psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3])
calibration_data.set_numpy(np)
# If input shapers are present in the CSV file, the frequency
# response is already normalized to input frequencies
if 'mzv' not in header:
calibration_data.normalize_to_frequencies()
return calibration_data
######################################################################
# Shaper calibration
######################################################################
# Find the best shaper parameters
def calibrate_shaper(datas, csv_output, max_smoothing):
helper = shaper_calibrate.ShaperCalibrate(printer=None)
if isinstance(datas[0], shaper_calibrate.CalibrationData):
calibration_data = datas[0]
for data in datas[1:]:
calibration_data.add_data(data)
else:
# Process accelerometer data
calibration_data = helper.process_accelerometer_data(datas[0])
for data in datas[1:]:
calibration_data.add_data(helper.process_accelerometer_data(data))
calibration_data.normalize_to_frequencies()
shaper, all_shapers, resp = helper.find_best_shaper(
calibration_data, max_smoothing, print)
if csv_output is not None:
helper.save_calibration_data(
csv_output, calibration_data, all_shapers)
return shaper.name, all_shapers, calibration_data, resp
######################################################################
# Plot frequency response and suggested input shapers
######################################################################
def plot_freq_response(lognames, calibration_data, shapers,
selected_shaper, max_freq):
freqs = calibration_data.freq_bins
psd = calibration_data.psd_sum[freqs <= max_freq]
px = calibration_data.psd_x[freqs <= max_freq]
py = calibration_data.psd_y[freqs <= max_freq]
pz = calibration_data.psd_z[freqs <= max_freq]
freqs = freqs[freqs <= max_freq]
fontP = matplotlib.font_manager.FontProperties()
fontP.set_size('small')
fig, ax = matplotlib.pyplot.subplots()
ax.set_xlabel('Frequency, Hz')
ax.set_xlim([0, max_freq])
ax.set_ylabel('Power spectral density')
ax.plot(freqs, psd, label='X+Y+Z', color='purple')
ax.plot(freqs, px, label='X', color='red')
ax.plot(freqs, py, label='Y', color='green')
ax.plot(freqs, pz, label='Z', color='blue')
title = "Frequency response and shapers (%s)" % (', '.join(lognames))
ax.set_title("\n".join(wrap(title, MAX_TITLE_LENGTH)))
ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(5))
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0))
ax.grid(which='major', color='grey')
ax.grid(which='minor', color='lightgrey')
ax2 = ax.twinx()
ax2.set_ylabel('Shaper vibration reduction (ratio)')
best_shaper_vals = None
for shaper in shapers:
label = "%s (%.1f Hz, vibr=%.1f%%, sm~=%.2f, accel<=%.f)" % (
shaper.name.upper(), shaper.freq,
shaper.vibrs * 100., shaper.smoothing,
round(shaper.max_accel / 100.) * 100.)
linestyle = 'dotted'
if shaper.name == selected_shaper:
linestyle = 'dashdot'
best_shaper_vals = shaper.vals
ax2.plot(freqs, shaper.vals, label=label, linestyle=linestyle)
ax.plot(freqs, psd * best_shaper_vals,
label='After\nshaper', color='cyan')
# A hack to add a human-readable shaper recommendation to legend
ax2.plot([], [], ' ',
label="Recommended shaper: %s" % (selected_shaper.upper()))
ax.legend(loc='upper left', prop=fontP)
ax2.legend(loc='upper right', prop=fontP)
fig.tight_layout()
return fig
######################################################################
# Startup
######################################################################
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] <logs>"
opts = optparse.OptionParser(usage)
opts.add_option("-o", "--output", type="string", dest="output",
default=None, help="filename of output graph")
opts.add_option("-c", "--csv", type="string", dest="csv",
default=None, help="filename of output csv file")
opts.add_option("-f", "--max_freq", type="float", default=200.,
help="maximum frequency to graph")
opts.add_option("-s", "--max_smoothing", type="float", default=None,
help="maximum shaper smoothing to allow")
opts.add_option("-w", "--width", type="float", dest="width",
default=8.3, help="width (inches) of the graph(s)")
opts.add_option("-l", "--height", type="float", dest="height",
default=11.6, help="height (inches) of the graph(s)")
options, args = opts.parse_args()
if len(args) < 1:
opts.error("Incorrect number of arguments")
if options.max_smoothing is not None and options.max_smoothing < 0.05:
opts.error("Too small max_smoothing specified (must be at least 0.05)")
# Parse data
datas = [parse_log(fn) for fn in args]
# Calibrate shaper and generate outputs
selected_shaper, shapers, calibration_data, resp = calibrate_shaper(
datas, options.csv, options.max_smoothing)
resp['logfile'] = args[0]
if not options.csv or options.output:
# Draw graph
setup_matplotlib(options.output is not None)
fig = plot_freq_response(args, calibration_data, shapers,
selected_shaper, options.max_freq)
# Show graph
if options.output is None:
matplotlib.pyplot.show()
else:
pathlib.Path(options.output).unlink(missing_ok=True)
fig.set_size_inches(options.width, options.height)
fig.savefig(options.output)
resp['png'] = options.output
print(json.dumps(resp))
print
if __name__ == '__main__':
main()

View file

@ -0,0 +1,573 @@
#!/usr/bin/env python3
#################################################
######## CoreXY BELTS CALIBRATION SCRIPT ########
#################################################
# Written by Frix_x#0161 #
# Be sure to make this script executable using SSH: type 'chmod +x ./graph_belts.py' when in the folder!
#####################################################################
################ !!! DO NOT EDIT BELOW THIS LINE !!! ################
#####################################################################
import optparse, matplotlib, sys, importlib, os, pathlib
from collections import namedtuple
import numpy as np
import matplotlib.pyplot, matplotlib.dates, matplotlib.font_manager
import matplotlib.ticker, matplotlib.gridspec, matplotlib.colors
import matplotlib.patches
import locale
import time
import glob
import shaper_calibrate
from datetime import datetime
matplotlib.use('Agg')
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # For paired peaks names
PEAKS_DETECTION_THRESHOLD = 0.20
CURVE_SIMILARITY_SIGMOID_K = 0.6
DC_GRAIN_OF_SALT_FACTOR = 0.75
DC_THRESHOLD_METRIC = 1.5e9
DC_MAX_UNPAIRED_PEAKS_ALLOWED = 4
# Define the SignalData namedtuple
SignalData = namedtuple('CalibrationData', ['freqs', 'psd', 'peaks', 'paired_peaks', 'unpaired_peaks'])
KLIPPAIN_COLORS = {
"purple": "#70088C",
"orange": "#FF8D32",
"dark_purple": "#150140",
"dark_orange": "#F24130",
"red_pink": "#F2055C"
}
# Set the best locale for time and date formating (generation of the titles)
try:
locale.setlocale(locale.LC_TIME, locale.getdefaultlocale())
except locale.Error:
locale.setlocale(locale.LC_TIME, 'C')
# Override the built-in print function to avoid problem in Klipper due to locale settings
original_print = print
def print_with_c_locale(*args, **kwargs):
original_locale = locale.setlocale(locale.LC_ALL, None)
locale.setlocale(locale.LC_ALL, 'C')
original_print(*args, **kwargs)
locale.setlocale(locale.LC_ALL, original_locale)
print = print_with_c_locale
def is_file_open(filepath):
for proc in os.listdir('/proc'):
if proc.isdigit():
for fd in glob.glob(f'/proc/{proc}/fd/*'):
try:
if os.path.samefile(fd, filepath):
return True
except FileNotFoundError:
# Klipper has already released the CSV file
pass
except PermissionError:
# Unable to check for this particular process due to permissions
pass
return False
######################################################################
# Computation of the PSD graph
######################################################################
# Calculate estimated "power spectral density" using existing Klipper tools
def calc_freq_response(data):
helper = shaper_calibrate.ShaperCalibrate(printer=None)
return helper.process_accelerometer_data(data)
# Calculate or estimate a "similarity" factor between two PSD curves and scale it to a percentage. This is
# used here to quantify how close the two belts path behavior and responses are close together.
def compute_curve_similarity_factor(signal1, signal2):
freqs1 = signal1.freqs
psd1 = signal1.psd
freqs2 = signal2.freqs
psd2 = signal2.psd
# Interpolate PSDs to match the same frequency bins and do a cross-correlation
psd2_interp = np.interp(freqs1, freqs2, psd2)
cross_corr = np.correlate(psd1, psd2_interp, mode='full')
# Find the peak of the cross-correlation and compute a similarity normalized by the energy of the signals
peak_value = np.max(cross_corr)
similarity = peak_value / (np.sqrt(np.sum(psd1**2) * np.sum(psd2_interp**2)))
# Apply sigmoid scaling to get better numbers and get a final percentage value
scaled_similarity = sigmoid_scale(-np.log(1 - similarity), CURVE_SIMILARITY_SIGMOID_K)
return scaled_similarity
# This find all the peaks in a curve by looking at when the derivative term goes from positive to negative
# Then only the peaks found above a threshold are kept to avoid capturing peaks in the low amplitude noise of a signal
def detect_peaks(psd, freqs, window_size=5, vicinity=3):
# Smooth the curve using a moving average to avoid catching peaks everywhere in noisy signals
kernel = np.ones(window_size) / window_size
smoothed_psd = np.convolve(psd, kernel, mode='valid')
mean_pad = [np.mean(psd[:window_size])] * (window_size // 2)
smoothed_psd = np.concatenate((mean_pad, smoothed_psd))
# Find peaks on the smoothed curve
smoothed_peaks = np.where((smoothed_psd[:-2] < smoothed_psd[1:-1]) & (smoothed_psd[1:-1] > smoothed_psd[2:]))[0] + 1
detection_threshold = PEAKS_DETECTION_THRESHOLD * psd.max()
smoothed_peaks = smoothed_peaks[smoothed_psd[smoothed_peaks] > detection_threshold]
# Refine peak positions on the original curve
refined_peaks = []
for peak in smoothed_peaks:
local_max = peak + np.argmax(psd[max(0, peak-vicinity):min(len(psd), peak+vicinity+1)]) - vicinity
refined_peaks.append(local_max)
return np.array(refined_peaks), freqs[refined_peaks]
# This function create pairs of peaks that are close in frequency on two curves (that are known
# to be resonances points and must be similar on both belts on a CoreXY kinematic)
def pair_peaks(peaks1, freqs1, psd1, peaks2, freqs2, psd2):
# Compute a dynamic detection threshold to filter and pair peaks efficiently
# even if the signal is very noisy (this get clipped to a maximum of 10Hz diff)
distances = []
for p1 in peaks1:
for p2 in peaks2:
distances.append(abs(freqs1[p1] - freqs2[p2]))
distances = np.array(distances)
median_distance = np.median(distances)
iqr = np.percentile(distances, 75) - np.percentile(distances, 25)
threshold = median_distance + 1.5 * iqr
threshold = min(threshold, 10)
# Pair the peaks using the dynamic thresold
paired_peaks = []
unpaired_peaks1 = list(peaks1)
unpaired_peaks2 = list(peaks2)
while unpaired_peaks1 and unpaired_peaks2:
min_distance = threshold + 1
pair = None
for p1 in unpaired_peaks1:
for p2 in unpaired_peaks2:
distance = abs(freqs1[p1] - freqs2[p2])
if distance < min_distance:
min_distance = distance
pair = (p1, p2)
if pair is None: # No more pairs below the threshold
break
p1, p2 = pair
paired_peaks.append(((p1, freqs1[p1], psd1[p1]), (p2, freqs2[p2], psd2[p2])))
unpaired_peaks1.remove(p1)
unpaired_peaks2.remove(p2)
return paired_peaks, unpaired_peaks1, unpaired_peaks2
######################################################################
# Computation of a basic signal spectrogram
######################################################################
def compute_spectrogram(data):
import scipy
N = data.shape[0]
Fs = N / (data[-1, 0] - data[0, 0])
# Round up to a power of 2 for faster FFT
M = 1 << int(.5 * Fs - 1).bit_length()
window = np.kaiser(M, 6.)
def _specgram(x):
x_detrended = x - np.mean(x) # Detrending by subtracting the mean value
return scipy.signal.spectrogram(
x_detrended, fs=Fs, window=window, nperseg=M, noverlap=M//2,
detrend='constant', scaling='density', mode='psd')
d = {'x': data[:, 1], 'y': data[:, 2], 'z': data[:, 3]}
f, t, pdata = _specgram(d['x'])
for axis in 'yz':
pdata += _specgram(d[axis])[2]
return pdata, t, f
######################################################################
# Computation of the differential spectrogram
######################################################################
# Interpolate source_data (2D) to match target_x and target_y in order to
# get similar time and frequency dimensions for the differential spectrogram
def interpolate_2d(target_x, target_y, source_x, source_y, source_data):
import scipy
# Create a grid of points in the source and target space
source_points = np.array([(x, y) for y in source_y for x in source_x])
target_points = np.array([(x, y) for y in target_y for x in target_x])
# Flatten the source data to match the flattened source points
source_values = source_data.flatten()
# Interpolate and reshape the interpolated data to match the target grid shape and replace NaN with zeros
interpolated_data = scipy.interpolate.griddata(source_points, source_values, target_points, method='nearest')
interpolated_data = interpolated_data.reshape((len(target_y), len(target_x)))
interpolated_data = np.nan_to_num(interpolated_data)
return interpolated_data
# Main logic function to combine two similar spectrogram - ie. from both belts paths - by substracting signals in order to create
# a new composite spectrogram. This result of a divergent but mostly centered new spectrogram (center will be white) with some colored zones
# highlighting differences in the belts paths. The summative spectrogram is used for the MHI calculation.
def combined_spectrogram(data1, data2):
pdata1, bins1, t1 = compute_spectrogram(data1)
pdata2, bins2, t2 = compute_spectrogram(data2)
# Interpolate the spectrograms
pdata2_interpolated = interpolate_2d(bins1, t1, bins2, t2, pdata2)
# Cobine them in two form: a summed diff for the MHI computation and a diverging diff for the spectrogram colors
combined_sum = np.abs(pdata1 - pdata2_interpolated)
combined_divergent = pdata1 - pdata2_interpolated
return combined_sum, combined_divergent, bins1, t1
# Compute a composite and highly subjective value indicating the "mechanical health of the printer (0 to 100%)" that represent the
# likelihood of mechanical issues on the printer. It is based on the differential spectrogram sum of gradient, salted with a bit
# of the estimated similarity cross-correlation from compute_curve_similarity_factor() and with a bit of the number of unpaired peaks.
# This result in a percentage value quantifying the machine behavior around the main resonances that give an hint if only touching belt tension
# will give good graphs or if there is a chance of mechanical issues in the background (above 50% should be considered as probably problematic)
def compute_mhi(combined_data, similarity_coefficient, num_unpaired_peaks):
# filtered_data = combined_data[combined_data > 100]
filtered_data = np.abs(combined_data)
# First compute a "total variability metric" based on the sum of the gradient that sum the magnitude of will emphasize regions of the
# spectrogram where there are rapid changes in magnitude (like the edges of resonance peaks).
total_variability_metric = np.sum(np.abs(np.gradient(filtered_data)))
# Scale the metric to a percentage using the threshold (found empirically on a large number of user data shared to me)
base_percentage = (np.log1p(total_variability_metric) / np.log1p(DC_THRESHOLD_METRIC)) * 100
# Adjust the percentage based on the similarity_coefficient to add a grain of salt
adjusted_percentage = base_percentage * (1 - DC_GRAIN_OF_SALT_FACTOR * (similarity_coefficient / 100))
# Adjust the percentage again based on the number of unpaired peaks to add a second grain of salt
peak_confidence = num_unpaired_peaks / DC_MAX_UNPAIRED_PEAKS_ALLOWED
final_percentage = (1 - peak_confidence) * adjusted_percentage + peak_confidence * 100
# Ensure the result lies between 0 and 100 by clipping the computed value
final_percentage = np.clip(final_percentage, 0, 100)
return final_percentage, mhi_lut(final_percentage)
# LUT to transform the MHI into a textual value easy to understand for the users of the script
def mhi_lut(mhi):
if 0 <= mhi <= 30:
return "Excellent mechanical health"
elif 30 < mhi <= 45:
return "Good mechanical health"
elif 45 < mhi <= 55:
return "Acceptable mechanical health"
elif 55 < mhi <= 70:
return "Potential signs of a mechanical issue"
elif 70 < mhi <= 85:
return "Likely a mechanical issue"
elif 85 < mhi <= 100:
return "Mechanical issue detected"
######################################################################
# Graphing
######################################################################
def plot_compare_frequency(ax, lognames, signal1, signal2, max_freq):
# Get the belt name for the legend to avoid putting the full file name
signal1_belt = (lognames[0].split('/')[-1]).split('_')[-1][0]
signal2_belt = (lognames[1].split('/')[-1]).split('_')[-1][0]
if signal1_belt == 'A' and signal2_belt == 'B':
signal1_belt += " (axis 1,-1)"
signal2_belt += " (axis 1, 1)"
elif signal1_belt == 'B' and signal2_belt == 'A':
signal1_belt += " (axis 1, 1)"
signal2_belt += " (axis 1,-1)"
else:
print("Warning: belts doesn't seem to have the correct name A and B (extracted from the filename.csv)")
# Plot the two belts PSD signals
ax.plot(signal1.freqs, signal1.psd, label="Belt " + signal1_belt, color=KLIPPAIN_COLORS['purple'])
ax.plot(signal2.freqs, signal2.psd, label="Belt " + signal2_belt, color=KLIPPAIN_COLORS['orange'])
# Trace the "relax region" (also used as a threshold to filter and detect the peaks)
psd_lowest_max = min(signal1.psd.max(), signal2.psd.max())
peaks_warning_threshold = PEAKS_DETECTION_THRESHOLD * psd_lowest_max
ax.axhline(y=peaks_warning_threshold, color='black', linestyle='--', linewidth=0.5)
ax.fill_between(signal1.freqs, 0, peaks_warning_threshold, color='green', alpha=0.15, label='Relax Region')
# Trace and annotate the peaks on the graph
paired_peak_count = 0
unpaired_peak_count = 0
offsets_table_data = []
for _, (peak1, peak2) in enumerate(signal1.paired_peaks):
label = ALPHABET[paired_peak_count]
amplitude_offset = abs(((signal2.psd[peak2[0]] - signal1.psd[peak1[0]]) / max(signal1.psd[peak1[0]], signal2.psd[peak2[0]])) * 100)
frequency_offset = abs(signal2.freqs[peak2[0]] - signal1.freqs[peak1[0]])
offsets_table_data.append([f"Peaks {label}", f"{frequency_offset:.1f} Hz", f"{amplitude_offset:.1f} %"])
ax.plot(signal1.freqs[peak1[0]], signal1.psd[peak1[0]], "x", color='black')
ax.plot(signal2.freqs[peak2[0]], signal2.psd[peak2[0]], "x", color='black')
ax.plot([signal1.freqs[peak1[0]], signal2.freqs[peak2[0]]], [signal1.psd[peak1[0]], signal2.psd[peak2[0]]], ":", color='gray')
ax.annotate(label + "1", (signal1.freqs[peak1[0]], signal1.psd[peak1[0]]),
textcoords="offset points", xytext=(8, 5),
ha='left', fontsize=13, color='black')
ax.annotate(label + "2", (signal2.freqs[peak2[0]], signal2.psd[peak2[0]]),
textcoords="offset points", xytext=(8, 5),
ha='left', fontsize=13, color='black')
paired_peak_count += 1
for peak in signal1.unpaired_peaks:
ax.plot(signal1.freqs[peak], signal1.psd[peak], "x", color='black')
ax.annotate(str(unpaired_peak_count + 1), (signal1.freqs[peak], signal1.psd[peak]),
textcoords="offset points", xytext=(8, 5),
ha='left', fontsize=13, color='red', weight='bold')
unpaired_peak_count += 1
for peak in signal2.unpaired_peaks:
ax.plot(signal2.freqs[peak], signal2.psd[peak], "x", color='black')
ax.annotate(str(unpaired_peak_count + 1), (signal2.freqs[peak], signal2.psd[peak]),
textcoords="offset points", xytext=(8, 5),
ha='left', fontsize=13, color='red', weight='bold')
unpaired_peak_count += 1
# Compute the similarity (using cross-correlation of the PSD signals)
ax2 = ax.twinx() # To split the legends in two box
ax2.yaxis.set_visible(False)
similarity_factor = compute_curve_similarity_factor(signal1, signal2)
ax2.plot([], [], ' ', label=f'Estimated similarity: {similarity_factor:.1f}%')
ax2.plot([], [], ' ', label=f'Number of unpaired peaks: {unpaired_peak_count}')
print(f"Belts estimated similarity: {similarity_factor:.1f}%")
# Setting axis parameters, grid and graph title
ax.set_xlabel('Frequency (Hz)')
ax.set_xlim([0, max_freq])
ax.set_ylabel('Power spectral density')
psd_highest_max = max(signal1.psd.max(), signal2.psd.max())
ax.set_ylim([0, psd_highest_max + psd_highest_max * 0.05])
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0))
ax.grid(which='major', color='grey')
ax.grid(which='minor', color='lightgrey')
fontP = matplotlib.font_manager.FontProperties()
fontP.set_size('small')
ax.set_title('Belts Frequency Profiles (estimated similarity: {:.1f}%)'.format(similarity_factor), fontsize=10, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
# Print the table of offsets ontop of the graph below the original legend (upper right)
if len(offsets_table_data) > 0:
columns = ["", "Frequency delta", "Amplitude delta", ]
offset_table = ax.table(cellText=offsets_table_data, colLabels=columns, bbox=[0.66, 0.75, 0.33, 0.15], loc='upper right', cellLoc='center')
offset_table.auto_set_font_size(False)
offset_table.set_fontsize(8)
offset_table.auto_set_column_width([0, 1, 2])
offset_table.set_zorder(100)
cells = [key for key in offset_table.get_celld().keys()]
for cell in cells:
offset_table[cell].set_facecolor('white')
offset_table[cell].set_alpha(0.6)
ax.legend(loc='upper left', prop=fontP)
ax2.legend(loc='center right', prop=fontP)
return similarity_factor, unpaired_peak_count
def plot_difference_spectrogram(ax, data1, data2, signal1, signal2, similarity_factor, max_freq):
combined_sum, combined_divergent, bins, t = combined_spectrogram(data1, data2)
# Compute the MHI value from the differential spectrogram sum of gradient, salted with
# the similarity factor and the number or unpaired peaks from the belts frequency profile
# Be careful, this value is highly opinionated and is pretty experimental!
mhi, textual_mhi = compute_mhi(combined_sum, similarity_factor, len(signal1.unpaired_peaks) + len(signal2.unpaired_peaks))
print(f"[experimental] Mechanical Health Indicator: {textual_mhi.lower()} ({mhi:.1f}%)")
ax.set_title(f"Differential Spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
ax.plot([], [], ' ', label=f'{textual_mhi} (experimental)')
# Draw the differential spectrogram with a specific custom norm to get orange or purple values where there is signal or white near zeros
colors = [KLIPPAIN_COLORS['dark_orange'], KLIPPAIN_COLORS['orange'], 'white', KLIPPAIN_COLORS['purple'], KLIPPAIN_COLORS['dark_purple']]
cm = matplotlib.colors.LinearSegmentedColormap.from_list('klippain_divergent', list(zip([0, 0.25, 0.5, 0.75, 1], colors)))
norm = matplotlib.colors.TwoSlopeNorm(vmin=np.min(combined_divergent), vcenter=0, vmax=np.max(combined_divergent))
ax.pcolormesh(t, bins, combined_divergent.T, cmap=cm, norm=norm, shading='gouraud')
ax.set_xlabel('Frequency (hz)')
ax.set_xlim([0., max_freq])
ax.set_ylabel('Time (s)')
ax.set_ylim([0, bins[-1]])
fontP = matplotlib.font_manager.FontProperties()
fontP.set_size('medium')
ax.legend(loc='best', prop=fontP)
# Plot vertical lines for unpaired peaks
unpaired_peak_count = 0
for _, peak in enumerate(signal1.unpaired_peaks):
ax.axvline(signal1.freqs[peak], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5)
ax.annotate(f"Peak {unpaired_peak_count + 1}", (signal1.freqs[peak], t[-1]*0.05),
textcoords="data", color=KLIPPAIN_COLORS['red_pink'], rotation=90, fontsize=10,
verticalalignment='bottom', horizontalalignment='right')
unpaired_peak_count +=1
for _, peak in enumerate(signal2.unpaired_peaks):
ax.axvline(signal2.freqs[peak], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5)
ax.annotate(f"Peak {unpaired_peak_count + 1}", (signal2.freqs[peak], t[-1]*0.05),
textcoords="data", color=KLIPPAIN_COLORS['red_pink'], rotation=90, fontsize=10,
verticalalignment='bottom', horizontalalignment='right')
unpaired_peak_count +=1
# Plot vertical lines and zones for paired peaks
for idx, (peak1, peak2) in enumerate(signal1.paired_peaks):
label = ALPHABET[idx]
x_min = min(peak1[1], peak2[1])
x_max = max(peak1[1], peak2[1])
ax.axvline(x_min, color=KLIPPAIN_COLORS['dark_purple'], linestyle='dotted', linewidth=1.5)
ax.axvline(x_max, color=KLIPPAIN_COLORS['dark_purple'], linestyle='dotted', linewidth=1.5)
ax.fill_between([x_min, x_max], 0, np.max(combined_divergent), color=KLIPPAIN_COLORS['dark_purple'], alpha=0.3)
ax.annotate(f"Peaks {label}", (x_min, t[-1]*0.05),
textcoords="data", color=KLIPPAIN_COLORS['dark_purple'], rotation=90, fontsize=10,
verticalalignment='bottom', horizontalalignment='right')
return
######################################################################
# Custom tools
######################################################################
# Simple helper to compute a sigmoid scalling (from 0 to 100%)
def sigmoid_scale(x, k=1):
return 1 / (1 + np.exp(-k * x)) * 100
# Original Klipper function to get the PSD data of a raw accelerometer signal
def compute_signal_data(data, max_freq):
calibration_data = calc_freq_response(data)
freqs = calibration_data.freq_bins[calibration_data.freq_bins <= max_freq]
psd = calibration_data.get_psd('all')[calibration_data.freq_bins <= max_freq]
peaks, _ = detect_peaks(psd, freqs)
return SignalData(freqs=freqs, psd=psd, peaks=peaks, paired_peaks=None, unpaired_peaks=None)
######################################################################
# Startup and main routines
######################################################################
def parse_log(logname):
with open(logname) as f:
for header in f:
if not header.startswith('#'):
break
if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
# Raw accelerometer data
return np.loadtxt(logname, comments='#', delimiter=',')
# Power spectral density data or shaper calibration data
raise ValueError("File %s does not contain raw accelerometer data and therefore "
"is not supported by this script. Please use the official Klipper "
"graph_accelerometer.py script to process it instead." % (logname,))
def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200., graph_spectogram=True, width=8.3, height=11.6):
for filename in lognames[:2]:
# Wait for the file handler to be released by Klipper
while is_file_open(filename):
time.sleep(2)
# Parse data
datas = [parse_log(fn) for fn in lognames]
if len(datas) > 2:
raise ValueError("Incorrect number of .csv files used (this function needs two files to compare them)")
# Compute calibration data for the two datasets with automatic peaks detection
signal1 = compute_signal_data(datas[0], max_freq)
signal2 = compute_signal_data(datas[1], max_freq)
# Pair the peaks across the two datasets
paired_peaks, unpaired_peaks1, unpaired_peaks2 = pair_peaks(signal1.peaks, signal1.freqs, signal1.psd,
signal2.peaks, signal2.freqs, signal2.psd)
signal1 = signal1._replace(paired_peaks=paired_peaks, unpaired_peaks=unpaired_peaks1)
signal2 = signal2._replace(paired_peaks=paired_peaks, unpaired_peaks=unpaired_peaks2)
if graph_spectogram:
fig = matplotlib.pyplot.figure()
gs = matplotlib.gridspec.GridSpec(2, 1, height_ratios=[4, 3])
ax1 = fig.add_subplot(gs[0])
ax2 = fig.add_subplot(gs[1])
else:
fig, ax1 = matplotlib.pyplot.subplots()
# Add title
try:
filename = lognames[0].split('/')[-1]
dt = datetime.strptime(f"{filename.split('_')[1]} {filename.split('_')[2]}", "%Y%m%d %H%M%S")
title_line2 = dt.strftime('%x %X')
except:
print("Warning: CSV filenames look to be different than expected (%s , %s)" % (lognames[0], lognames[1]))
title_line2 = lognames[0].split('/')[-1] + " / " + lognames[1].split('/')[-1]
fig.suptitle(title_line2)
# Plot the graphs
similarity_factor, _ = plot_compare_frequency(ax1, lognames, signal1, signal2, max_freq)
if graph_spectogram:
plot_difference_spectrogram(ax2, datas[0], datas[1], signal1, signal2, similarity_factor, max_freq)
fig.set_size_inches(width, height)
fig.tight_layout()
fig.subplots_adjust(top=0.89)
return fig
def main():
# Parse command-line arguments
usage = "%prog [options] <raw logs>"
opts = optparse.OptionParser(usage)
opts.add_option("-o", "--output", type="string", dest="output",
default=None, help="filename of output graph")
opts.add_option("-f", "--max_freq", type="float", default=200.,
help="maximum frequency to graph")
opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir",
default="~/klipper", help="main klipper directory")
opts.add_option("-n", "--no_spectogram", action="store_false", dest="no_spectogram",
default=True, help="disable plotting of spectogram")
opts.add_option("-w", "--width", type="float", dest="width",
default=8.3, help="width (inches) of the graph(s)")
opts.add_option("-l", "--height", type="float", dest="height",
default=11.6, help="height (inches) of the graph(s)")
options, args = opts.parse_args()
if len(args) < 1:
opts.error("Incorrect number of arguments")
if options.output is None:
opts.error("You must specify an output file.png to use the script (option -o)")
fig = belts_calibration(args, options.klipperdir, options.max_freq, options.no_spectogram,
options.width, options.height)
pathlib.Path(options.output).unlink(missing_ok=True)
fig.savefig(options.output)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,372 @@
# Automatic calibration of input shapers
#
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import collections, importlib, logging, math, multiprocessing, traceback
import shaper_defs
MIN_FREQ = 5.
MAX_FREQ = 200.
WINDOW_T_SEC = 0.5
MAX_SHAPER_FREQ = 150.
TEST_DAMPING_RATIOS=[0.075, 0.1, 0.15]
AUTOTUNE_SHAPERS = ['zv', 'mzv', 'ei', '2hump_ei', '3hump_ei']
######################################################################
# Frequency response calculation and shaper auto-tuning
######################################################################
class CalibrationData:
def __init__(self, freq_bins, psd_sum, psd_x, psd_y, psd_z):
self.freq_bins = freq_bins
self.psd_sum = psd_sum
self.psd_x = psd_x
self.psd_y = psd_y
self.psd_z = psd_z
self._psd_list = [self.psd_sum, self.psd_x, self.psd_y, self.psd_z]
self._psd_map = {'x': self.psd_x, 'y': self.psd_y, 'z': self.psd_z,
'all': self.psd_sum}
self.data_sets = 1
def add_data(self, other):
np = self.numpy
joined_data_sets = self.data_sets + other.data_sets
for psd, other_psd in zip(self._psd_list, other._psd_list):
# `other` data may be defined at different frequency bins,
# interpolating to fix that.
other_normalized = other.data_sets * np.interp(
self.freq_bins, other.freq_bins, other_psd)
psd *= self.data_sets
psd[:] = (psd + other_normalized) * (1. / joined_data_sets)
self.data_sets = joined_data_sets
def set_numpy(self, numpy):
self.numpy = numpy
def normalize_to_frequencies(self):
for psd in self._psd_list:
# Avoid division by zero errors
psd /= self.freq_bins + .1
# Remove low-frequency noise
psd[self.freq_bins < MIN_FREQ] = 0.
def get_psd(self, axis='all'):
return self._psd_map[axis]
CalibrationResult = collections.namedtuple(
'CalibrationResult',
('name', 'freq', 'vals', 'vibrs', 'smoothing', 'score', 'max_accel'))
class ShaperCalibrate:
def __init__(self, printer):
self.printer = printer
self.error = printer.command_error if printer else Exception
try:
self.numpy = importlib.import_module('numpy')
except ImportError:
raise self.error(
"Failed to import `numpy` module, make sure it was "
"installed via `~/klippy-env/bin/pip install` (refer to "
"docs/Measuring_Resonances.md for more details).")
def background_process_exec(self, method, args):
if self.printer is None:
return method(*args)
import queuelogger
parent_conn, child_conn = multiprocessing.Pipe()
def wrapper():
queuelogger.clear_bg_logging()
try:
res = method(*args)
except:
child_conn.send((True, traceback.format_exc()))
child_conn.close()
return
child_conn.send((False, res))
child_conn.close()
# Start a process to perform the calculation
calc_proc = multiprocessing.Process(target=wrapper)
calc_proc.daemon = True
calc_proc.start()
# Wait for the process to finish
reactor = self.printer.get_reactor()
gcode = self.printer.lookup_object("gcode")
eventtime = last_report_time = reactor.monotonic()
while calc_proc.is_alive():
if eventtime > last_report_time + 5.:
last_report_time = eventtime
gcode.respond_info("Wait for calculations..", log=False)
eventtime = reactor.pause(eventtime + .1)
# Return results
is_err, res = parent_conn.recv()
if is_err:
raise self.error("Error in remote calculation: %s" % (res,))
calc_proc.join()
parent_conn.close()
return res
def _split_into_windows(self, x, window_size, overlap):
# Memory-efficient algorithm to split an input 'x' into a series
# of overlapping windows
step_between_windows = window_size - overlap
n_windows = (x.shape[-1] - overlap) // step_between_windows
shape = (window_size, n_windows)
strides = (x.strides[-1], step_between_windows * x.strides[-1])
return self.numpy.lib.stride_tricks.as_strided(
x, shape=shape, strides=strides, writeable=False)
def _psd(self, x, fs, nfft):
# Calculate power spectral density (PSD) using Welch's algorithm
np = self.numpy
window = np.kaiser(nfft, 6.)
# Compensation for windowing loss
scale = 1.0 / (window**2).sum()
# Split into overlapping windows of size nfft
overlap = nfft // 2
x = self._split_into_windows(x, nfft, overlap)
# First detrend, then apply windowing function
x = window[:, None] * (x - np.mean(x, axis=0))
# Calculate frequency response for each window using FFT
result = np.fft.rfft(x, n=nfft, axis=0)
result = np.conjugate(result) * result
result *= scale / fs
# For one-sided FFT output the response must be doubled, except
# the last point for unpaired Nyquist frequency (assuming even nfft)
# and the 'DC' term (0 Hz)
result[1:-1,:] *= 2.
# Welch's algorithm: average response over windows
psd = result.real.mean(axis=-1)
# Calculate the frequency bins
freqs = np.fft.rfftfreq(nfft, 1. / fs)
return freqs, psd
def calc_freq_response(self, raw_values):
np = self.numpy
if raw_values is None:
return None
if isinstance(raw_values, np.ndarray):
data = raw_values
else:
samples = raw_values.get_samples()
if not samples:
return None
data = np.array(samples)
N = data.shape[0]
T = data[-1,0] - data[0,0]
SAMPLING_FREQ = N / T
# Round up to the nearest power of 2 for faster FFT
M = 1 << int(SAMPLING_FREQ * WINDOW_T_SEC - 1).bit_length()
if N <= M:
return None
# Calculate PSD (power spectral density) of vibrations per
# frequency bins (the same bins for X, Y, and Z)
fx, px = self._psd(data[:,1], SAMPLING_FREQ, M)
fy, py = self._psd(data[:,2], SAMPLING_FREQ, M)
fz, pz = self._psd(data[:,3], SAMPLING_FREQ, M)
return CalibrationData(fx, px+py+pz, px, py, pz)
def process_accelerometer_data(self, data):
calibration_data = self.background_process_exec(
self.calc_freq_response, (data,))
if calibration_data is None:
raise self.error(
"Internal error processing accelerometer data %s" % (data,))
calibration_data.set_numpy(self.numpy)
return calibration_data
def _estimate_shaper(self, shaper, test_damping_ratio, test_freqs):
np = self.numpy
A, T = np.array(shaper[0]), np.array(shaper[1])
inv_D = 1. / A.sum()
omega = 2. * math.pi * test_freqs
damping = test_damping_ratio * omega
omega_d = omega * math.sqrt(1. - test_damping_ratio**2)
W = A * np.exp(np.outer(-damping, (T[-1] - T)))
S = W * np.sin(np.outer(omega_d, T))
C = W * np.cos(np.outer(omega_d, T))
return np.sqrt(S.sum(axis=1)**2 + C.sum(axis=1)**2) * inv_D
def _estimate_remaining_vibrations(self, shaper, test_damping_ratio,
freq_bins, psd):
vals = self._estimate_shaper(shaper, test_damping_ratio, freq_bins)
# The input shaper can only reduce the amplitude of vibrations by
# SHAPER_VIBRATION_REDUCTION times, so all vibrations below that
# threshold can be igonred
vibr_threshold = psd.max() / shaper_defs.SHAPER_VIBRATION_REDUCTION
remaining_vibrations = self.numpy.maximum(
vals * psd - vibr_threshold, 0).sum()
all_vibrations = self.numpy.maximum(psd - vibr_threshold, 0).sum()
return (remaining_vibrations / all_vibrations, vals)
def _get_shaper_smoothing(self, shaper, accel=5000, scv=5.):
half_accel = accel * .5
A, T = shaper
inv_D = 1. / sum(A)
n = len(T)
# Calculate input shaper shift
ts = sum([A[i] * T[i] for i in range(n)]) * inv_D
# Calculate offset for 90 and 180 degrees turn
offset_90 = offset_180 = 0.
for i in range(n):
if T[i] >= ts:
# Calculate offset for one of the axes
offset_90 += A[i] * (scv + half_accel * (T[i]-ts)) * (T[i]-ts)
offset_180 += A[i] * half_accel * (T[i]-ts)**2
offset_90 *= inv_D * math.sqrt(2.)
offset_180 *= inv_D
return max(offset_90, offset_180)
def fit_shaper(self, shaper_cfg, calibration_data, max_smoothing):
np = self.numpy
test_freqs = np.arange(shaper_cfg.min_freq, MAX_SHAPER_FREQ, .2)
freq_bins = calibration_data.freq_bins
psd = calibration_data.psd_sum[freq_bins <= MAX_FREQ]
freq_bins = freq_bins[freq_bins <= MAX_FREQ]
best_res = None
results = []
for test_freq in test_freqs[::-1]:
shaper_vibrations = 0.
shaper_vals = np.zeros(shape=freq_bins.shape)
shaper = shaper_cfg.init_func(
test_freq, shaper_defs.DEFAULT_DAMPING_RATIO)
shaper_smoothing = self._get_shaper_smoothing(shaper)
if max_smoothing and shaper_smoothing > max_smoothing and best_res:
return best_res
# Exact damping ratio of the printer is unknown, pessimizing
# remaining vibrations over possible damping values
for dr in TEST_DAMPING_RATIOS:
vibrations, vals = self._estimate_remaining_vibrations(
shaper, dr, freq_bins, psd)
shaper_vals = np.maximum(shaper_vals, vals)
if vibrations > shaper_vibrations:
shaper_vibrations = vibrations
max_accel = self.find_shaper_max_accel(shaper)
# The score trying to minimize vibrations, but also accounting
# the growth of smoothing. The formula itself does not have any
# special meaning, it simply shows good results on real user data
shaper_score = shaper_smoothing * (shaper_vibrations**1.5 +
shaper_vibrations * .2 + .01)
results.append(
CalibrationResult(
name=shaper_cfg.name, freq=test_freq, vals=shaper_vals,
vibrs=shaper_vibrations, smoothing=shaper_smoothing,
score=shaper_score, max_accel=max_accel))
if best_res is None or best_res.vibrs > results[-1].vibrs:
# The current frequency is better for the shaper.
best_res = results[-1]
# Try to find an 'optimal' shapper configuration: the one that is not
# much worse than the 'best' one, but gives much less smoothing
selected = best_res
for res in results[::-1]:
if res.vibrs < best_res.vibrs * 1.1 and res.score < selected.score:
selected = res
return selected
def _bisect(self, func):
left = right = 1.
while not func(left):
right = left
left *= .5
if right == left:
while func(right):
right *= 2.
while right - left > 1e-8:
middle = (left + right) * .5
if func(middle):
left = middle
else:
right = middle
return left
def find_shaper_max_accel(self, shaper):
# Just some empirically chosen value which produces good projections
# for max_accel without much smoothing
TARGET_SMOOTHING = 0.12
max_accel = self._bisect(lambda test_accel: self._get_shaper_smoothing(
shaper, test_accel) <= TARGET_SMOOTHING)
return max_accel
def find_best_shaper(self, calibration_data, max_smoothing, logger=None):
best_shaper = None
all_shapers = []
resp = {}
for shaper_cfg in shaper_defs.INPUT_SHAPERS:
if shaper_cfg.name not in AUTOTUNE_SHAPERS:
continue
shaper = self.background_process_exec(self.fit_shaper, (
shaper_cfg, calibration_data, max_smoothing))
if logger is not None:
resp[shaper.name] = {
'freq': shaper.freq,
'vib': shaper.vibrs * 100.,
'smooth': shaper.smoothing,
'max_acel': round(shaper.max_accel / 100.) * 100.
}
all_shapers.append(shaper)
if (best_shaper is None or shaper.score * 1.2 < best_shaper.score or
(shaper.score * 1.05 < best_shaper.score and
shaper.smoothing * 1.1 < best_shaper.smoothing)):
# Either the shaper significantly improves the score (by 20%),
# or it improves the score and smoothing (by 5% and 10% resp.)
best_shaper = shaper
return best_shaper, all_shapers, {'shapers': resp, 'best': best_shaper.name}
def save_params(self, configfile, axis, shaper_name, shaper_freq):
if axis == 'xy':
self.save_params(configfile, 'x', shaper_name, shaper_freq)
self.save_params(configfile, 'y', shaper_name, shaper_freq)
else:
configfile.set('input_shaper', 'shaper_type_'+axis, shaper_name)
configfile.set('input_shaper', 'shaper_freq_'+axis,
'%.1f' % (shaper_freq,))
def apply_params(self, input_shaper, axis, shaper_name, shaper_freq):
if axis == 'xy':
self.apply_params(input_shaper, 'x', shaper_name, shaper_freq)
self.apply_params(input_shaper, 'y', shaper_name, shaper_freq)
return
gcode = self.printer.lookup_object("gcode")
axis = axis.upper()
input_shaper.cmd_SET_INPUT_SHAPER(gcode.create_gcode_command(
"SET_INPUT_SHAPER", "SET_INPUT_SHAPER", {
"SHAPER_TYPE_" + axis: shaper_name,
"SHAPER_FREQ_" + axis: shaper_freq}))
def save_calibration_data(self, output, calibration_data, shapers=None):
try:
with open(output, "w") as csvfile:
csvfile.write("freq,psd_x,psd_y,psd_z,psd_xyz")
if shapers:
for shaper in shapers:
csvfile.write(",%s(%.1f)" % (shaper.name, shaper.freq))
csvfile.write("\n")
num_freqs = calibration_data.freq_bins.shape[0]
for i in range(num_freqs):
if calibration_data.freq_bins[i] >= MAX_FREQ:
break
csvfile.write("%.1f,%.3e,%.3e,%.3e,%.3e" % (
calibration_data.freq_bins[i],
calibration_data.psd_x[i],
calibration_data.psd_y[i],
calibration_data.psd_z[i],
calibration_data.psd_sum[i]))
if shapers:
for shaper in shapers:
csvfile.write(",%.3f" % (shaper.vals[i],))
csvfile.write("\n")
except IOError as e:
raise self.error("Error writing to file '%s': %s", output, str(e))

View file

@ -0,0 +1,102 @@
# Definitions of the supported input shapers
#
# Copyright (C) 2020-2021 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import collections, math
SHAPER_VIBRATION_REDUCTION=20.
DEFAULT_DAMPING_RATIO = 0.1
InputShaperCfg = collections.namedtuple(
'InputShaperCfg', ('name', 'init_func', 'min_freq'))
def get_none_shaper():
return ([], [])
def get_zv_shaper(shaper_freq, damping_ratio):
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
A = [1., K]
T = [0., .5*t_d]
return (A, T)
def get_zvd_shaper(shaper_freq, damping_ratio):
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
A = [1., 2.*K, K**2]
T = [0., .5*t_d, t_d]
return (A, T)
def get_mzv_shaper(shaper_freq, damping_ratio):
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-.75 * damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
a1 = 1. - 1. / math.sqrt(2.)
a2 = (math.sqrt(2.) - 1.) * K
a3 = a1 * K * K
A = [a1, a2, a3]
T = [0., .375*t_d, .75*t_d]
return (A, T)
def get_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
a1 = .25 * (1. + v_tol)
a2 = .5 * (1. - v_tol) * K
a3 = a1 * K * K
A = [a1, a2, a3]
T = [0., .5*t_d, t_d]
return (A, T)
def get_2hump_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
V2 = v_tol**2
X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.)
a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X)
a2 = (.5 - a1) * K
a3 = a2 * K
a4 = a1 * K * K * K
A = [a1, a2, a3, a4]
T = [0., .5*t_d, t_d, 1.5*t_d]
return (A, T)
def get_3hump_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
K2 = K*K
a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol))
a2 = 0.25 * (1. - v_tol) * K
a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2
a4 = a2 * K2
a5 = a1 * K2 * K2
A = [a1, a2, a3, a4, a5]
T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d]
return (A, T)
# min_freq for each shaper is chosen to have projected max_accel ~= 1500
INPUT_SHAPERS = [
InputShaperCfg('zv', get_zv_shaper, min_freq=21.),
InputShaperCfg('mzv', get_mzv_shaper, min_freq=23.),
InputShaperCfg('zvd', get_zvd_shaper, min_freq=29.),
InputShaperCfg('ei', get_ei_shaper, min_freq=29.),
InputShaperCfg('2hump_ei', get_2hump_ei_shaper, min_freq=39.),
InputShaperCfg('3hump_ei', get_3hump_ei_shaper, min_freq=48.),
]

View file

@ -0,0 +1,93 @@
###########################################
# Adaptive Meshing for Creality K1 Series
###########################################
[gcode_macro BED_MESH_CALIBRATE]
rename_existing: _BED_MESH_CALIBRATE
gcode:
{% set all_points = printer.exclude_object.objects | map(attribute='polygon') | sum(start=[]) %}
{% set bed_mesh_min = printer.configfile.settings.bed_mesh.mesh_min %}
{% set bed_mesh_max = printer.configfile.settings.bed_mesh.mesh_max %}
{% set probe_count = printer.configfile.settings.bed_mesh.probe_count %}
{% set kamp_settings = printer["gcode_macro _KAMP_Settings"] %}
{% set verbose_enable = kamp_settings.verbose_enable | abs %}
{% set mesh_margin = kamp_settings.mesh_margin | float %}
{% set fuzz_amount = kamp_settings.fuzz_amount | float %}
{% set probe_count = probe_count if probe_count|length > 1 else probe_count * 2 %}
{% set max_probe_point_distance_x = ( bed_mesh_max[0] - bed_mesh_min[0] ) / (probe_count[0] - 1) %}
{% set max_probe_point_distance_y = ( bed_mesh_max[1] - bed_mesh_min[1] ) / (probe_count[1] - 1) %}
{% set x_min = all_points | map(attribute=0) | min | default(bed_mesh_min[0]) %}
{% set y_min = all_points | map(attribute=1) | min | default(bed_mesh_min[1]) %}
{% set x_max = all_points | map(attribute=0) | max | default(bed_mesh_max[0]) %}
{% set y_max = all_points | map(attribute=1) | max | default(bed_mesh_max[1]) %}
{% set fuzz_range = range((0) | int, (fuzz_amount * 100) | int + 1) %}
{% set adapted_x_min = x_min - mesh_margin - (fuzz_range | random / 100.0) %}
{% set adapted_y_min = y_min - mesh_margin - (fuzz_range | random / 100.0) %}
{% set adapted_x_max = x_max + mesh_margin + (fuzz_range | random / 100.0) %}
{% set adapted_y_max = y_max + mesh_margin + (fuzz_range | random / 100.0) %}
{% set adapted_x_min = [adapted_x_min , bed_mesh_min[0]] | max %}
{% set adapted_y_min = [adapted_y_min , bed_mesh_min[1]] | max %}
{% set adapted_x_max = [adapted_x_max , bed_mesh_max[0]] | min %}
{% set adapted_y_max = [adapted_y_max , bed_mesh_max[1]] | min %}
{% set points_x = (((adapted_x_max - adapted_x_min) / max_probe_point_distance_x) | round(method='ceil') | int) + 1 %}
{% set points_y = (((adapted_y_max - adapted_y_min) / max_probe_point_distance_y) | round(method='ceil') | int) + 1 %}
{% if (points_x > points_y) %}
{% set points_y = points_x %}
{% endif %}
{% if (points_x < points_y) %}
{% set points_x = points_y %}
{% endif %}
{% if (([points_x, points_y]|max) > 6) %}
{% set algorithm = "bicubic" %}
{% set min_points = 4 %}
{% else %}
{% set algorithm = "lagrange" %}
{% set min_points = 3 %}
{% endif %}
{% set points_x = [points_x , min_points]|max %}
{% set points_y = [points_y , min_points]|max %}
{% set points_x = [points_x , probe_count[0]]|min %}
{% set points_y = [points_y , probe_count[1]]|min %}
{% if verbose_enable == True %}
{% if printer.exclude_object.objects != [] %}
RESPOND TYPE=command MSG="Algorithm: {algorithm}"
RESPOND TYPE=command MSG="Default probe count: {probe_count[0]},{probe_count[1]}"
RESPOND TYPE=command MSG="Adapted probe count: {points_x},{points_y}"
RESPOND TYPE=command MSG="Default mesh bounds: {bed_mesh_min[0]},{bed_mesh_min[1]}, {bed_mesh_max[0]},{bed_mesh_max[1]}"
{% if mesh_margin > 0 %}
RESPOND TYPE=command MSG="Mesh margin is {mesh_margin}, mesh bounds extended by {mesh_margin}mm."
{% else %}
RESPOND TYPE=command MSG="Mesh margin is 0, margin not increased."
{% endif %}
{% if fuzz_amount > 0 %}
RESPOND TYPE=command MSG="Mesh point fuzzing enabled, points fuzzed up to {fuzz_amount}mm"
{% else %}
RESPOND TYPE=command MSG="Fuzz amount is 0, mesh points not fuzzed."
{% endif %}
RESPOND TYPE=command MSG="Adapted mesh bounds: {adapted_x_min},{adapted_y_min}, {adapted_x_max},{adapted_y_max}"
RESPOND TYPE=command MSG="KAMP adjustments successful. Happy KAMPing!"
{% else %}
RESPOND TYPE=command MSG="No object detected! Make sure you have enabled Exclude Objets setting in your slicer. Using Full Bed Mesh."
G4 P5000
{% endif %}
{% endif %}
_BED_MESH_CALIBRATE mesh_min={adapted_x_min},{adapted_y_min} mesh_max={adapted_x_max},{adapted_y_max} ALGORITHM={algorithm} PROBE_COUNT={points_x},{points_y}

View file

@ -0,0 +1,40 @@
###########################################
# KAMP Settings for Creality K1 Series
###########################################
# Below you can enable or disable specific configuration files depending on what you want KAMP to do:
[include Start_Print.cfg] # START_PRINT macro for Creality K1 Series.
[include Adaptive_Meshing.cfg] # Adaptive Meshing configurations.
[include Line_Purge.cfg] # Adaptive Line Purging configurations.
[include Smart_Park.cfg] # Smart Park feature, which parks the printhead near the print area for final heating.
#[include Prusa_Slicer.cfg] # Enable this if you use Prusa Slicer, it's the necessary macros to enable Exclude Objects functionality.
[respond] # Necessary to receive messages from KAMP
[gcode_macro _KAMP_Settings]
description: This macro contains all adjustable settings for KAMP
# The following variables are settings for KAMP as a whole:
variable_verbose_enable: True # Set to True to enable KAMP information output when running. This is useful for debugging.
# The following variables are for adjusting Adaptive Meshing settings for KAMP:
variable_mesh_margin: 0 # Expands the mesh size in millimeters if desired. Leave at 0 to disable.
variable_fuzz_amount: 0 # Slightly randomizes mesh points to spread out wear from nozzle-based probes. Leave at 0 to disable.
# The following variables are for adjusting Adaptive Line Purging settings for KAMP:
variable_purge_height: 0.8 # Z position of nozzle during purge. Default is 0.8.
variable_tip_distance: 0 # Distance between tip of filament and nozzle before purge. Should be similar to PRINT_END final retract amount. Default is 0.
variable_purge_margin: 10 # Distance the purge will be in front of the print area. Default is 10.
variable_purge_amount: 50 # Amount of filament to be purged prior to printing. Default is 50.
variable_flow_rate: 12 # Flow rate of purge in mm3/s. Default is 12.
# The following variables are for adjusting the Smart Park feature for KAMP, which will park the printhead near the print area at a specified height:
variable_smart_park_height: 10 # Z position for Smart Park. Default is 10.
gcode:
RESPOND TYPE=command MSG="Running the KAMP_Settings macro does nothing, it's only used for storing KAMP settings."

200
files/kamp/Line_Purge.cfg Normal file
View file

@ -0,0 +1,200 @@
###########################################
# Line Purge for Creality K1 Series
###########################################
[gcode_macro _LINE_PURGE]
description: A purge macro that adapts to be near your actual printed objects
gcode:
{% set travel_speed = (printer.toolhead.max_velocity) * 60 | float %}
{% set cross_section = printer.configfile.settings.extruder.max_extrude_cross_section | float %}
{% if printer.firmware_retraction is defined %}
{% set RETRACT = G10 | string %}
{% set UNRETRACT = G11 | string %}
{% else %}
{% set RETRACT = 'G1 E-0.5 F2400' | string %}
{% set UNRETRACT = 'G1 E0.5 F2400' | string %}
{% endif %}
{% set bed_x_max = printer["gcode_macro PRINTER_PARAM"].max_x_position | float %}
{% set bed_y_max = printer["gcode_macro PRINTER_PARAM"].max_y_position | float %}
{% set verbose_enable = printer["gcode_macro _KAMP_Settings"].verbose_enable | abs %}
{% set purge_height = printer["gcode_macro _KAMP_Settings"].purge_height | float %}
{% set tip_distance = printer["gcode_macro _KAMP_Settings"].tip_distance | float %}
{% set purge_margin = printer["gcode_macro _KAMP_Settings"].purge_margin | float %}
{% set purge_amount = printer["gcode_macro _KAMP_Settings"].purge_amount | float %}
{% set flow_rate = printer["gcode_macro _KAMP_Settings"].flow_rate | float %}
{% set rapid_move = 10 %}
{% set all_points = printer.exclude_object.objects | map(attribute='polygon') | sum(start=[]) %}
{% set purge_x_min = (all_points | map(attribute=0) | min | default(0)) %}
{% set purge_x_max = (all_points | map(attribute=0) | max | default(0)) %}
{% set purge_y_min = (all_points | map(attribute=1) | min | default(0)) %}
{% set purge_y_max = (all_points | map(attribute=1) | max | default(0)) %}
{% set detect_object = purge_x_min + purge_x_max + purge_y_min + purge_y_max %}
{% set purge_x_center = ([((purge_x_max + purge_x_min) / 2) - (purge_amount / 2), 0] | max) %}
{% set purge_y_center = ([((purge_y_max + purge_y_min) / 2) - (purge_amount / 2), 0] | max) %}
{% if (purge_x_center + purge_amount + rapid_move) > bed_x_max %}
{% set purge_x_center = (bed_x_max - (purge_amount + rapid_move)) %}
{% endif %}
{% if (purge_y_center + purge_amount + rapid_move) > bed_y_max %}
{% set purge_y_center = (bed_y_max - (purge_amount + rapid_move)) %}
{% endif %}
{% set purge_x_origin_low = (purge_x_min - purge_margin) %}
{% set purge_x_origin_high = (purge_x_max + purge_margin) %}
{% set purge_y_origin_low = (purge_y_min - purge_margin) %}
{% set purge_y_origin_high = (purge_y_max + purge_margin) %}
{% set purge_move_speed = (flow_rate / 5.0) * 60 | float %}
{% if cross_section < 5 %}
RESPOND TYPE=command MSG="[Extruder] max_extrude_cross_section is insufficient for line purge, please set it to 5 or greater. Purge skipped."
{% else %}
{% if verbose_enable == True %}
RESPOND TYPE=command MSG="Moving filament tip {tip_distance}mm"
{% endif %}
{% if detect_object == 0 %}
RESPOND TYPE=command MSG="No object detected! Using classic purge line."
{% elif purge_y_origin_low > 0 %}
RESPOND TYPE=command MSG="KAMP line purge starting at {purge_x_center}, {purge_y_origin_low} and purging {purge_amount}mm of filament, requested flow rate is {flow_rate}mm3/s."
{% elif purge_x_origin_low > 0 %}
RESPOND TYPE=command MSG="KAMP line purge starting at {purge_x_origin_low}, {purge_y_center} and purging {purge_amount}mm of filament, requested flow rate is {flow_rate}mm3/s."
{% elif purge_y_origin_high < bed_y_max %}
RESPOND TYPE=command MSG="KAMP line purge starting at {purge_x_center}, {purge_y_origin_high} and purging {purge_amount}mm of filament, requested flow rate is {flow_rate}mm3/s."
{% elif purge_x_origin_high < bed_x_max %}
RESPOND TYPE=command MSG="KAMP line purge starting at {purge_x_origin_high}, {purge_y_center} and purging {purge_amount}mm of filament, requested flow rate is {flow_rate}mm3/s."
{% else %}
RESPOND TYPE=command MSG="No space for purge line! Using classic purge line."
{% endif %}
SAVE_GCODE_STATE NAME=Prepurge_State
{% if detect_object == 0 %}
G92 E0
G1 Z0.1 F600
M83
{RETRACT}
SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=5
M204 S12000
SET_VELOCITY_LIMIT ACCEL_TO_DECEL=6000
M220 S100
M221 S100
G1 Z2.0 F1200
G1 X0.1 Y20 Z0.3 F6000.0
G1 X0.1 Y180.0 Z0.3 F3000.0 E10.0
G1 X0.4 Y180.0 Z0.3 F3000.0
G1 X0.4 Y20.0 Z0.3 F3000.0 E10.0
G1 Y10.0 F3000.0
G1 Z2.0 F3000.0
G92 E0
M82
G1 F12000
G21
{% elif purge_y_origin_low > 0 %}
G92 E0
G0 F{travel_speed}
G90
G0 X{purge_x_center} Y{purge_y_origin_low}
G0 Z{purge_height}
M83
G1 E{tip_distance} F{purge_move_speed}
G1 X{purge_x_center + purge_amount} E{purge_amount} F{purge_move_speed}
{RETRACT}
G0 X{purge_x_center + purge_amount + rapid_move} F{travel_speed}
G92 E0
M82
G0 Z{purge_height * 2} F{travel_speed}
{% elif purge_x_origin_low > 0 %}
G92 E0
G0 F{travel_speed}
G90
G0 X{purge_x_origin_low} Y{purge_y_center}
G0 Z{purge_height}
M83
G1 E{tip_distance} F{purge_move_speed}
G1 Y{purge_y_center + purge_amount} E{purge_amount} F{purge_move_speed}
{RETRACT}
G0 Y{purge_y_center + purge_amount + rapid_move} F{travel_speed}
G92 E0
M82
G0 Z{purge_height * 2} F{travel_speed}
{% elif purge_y_origin_high < bed_y_max %}
G92 E0
G0 F{travel_speed}
G90
G0 X{purge_x_center} Y{purge_y_origin_high}
G0 Z{purge_height}
M83
G1 E{tip_distance} F{purge_move_speed}
G1 X{purge_x_center + purge_amount} E{purge_amount} F{purge_move_speed}
{RETRACT}
G0 X{purge_x_center + purge_amount + rapid_move} F{travel_speed}
G92 E0
M82
G0 Z{purge_height * 2} F{travel_speed}
{% elif purge_x_origin_high < bed_x_max %}
G92 E0
G0 F{travel_speed}
G90
G0 X{purge_x_origin_high} Y{purge_y_center}
G0 Z{purge_height}
M83
G1 E{tip_distance} F{purge_move_speed}
G1 Y{purge_y_center + purge_amount} E{purge_amount} F{purge_move_speed}
{RETRACT}
G0 Y{purge_y_center + purge_amount + rapid_move} F{travel_speed}
G92 E0
M82
G0 Z{purge_height * 2} F{travel_speed}
{% else %}
G92 E0
G1 Z0.1 F600
M83
{RETRACT}
SET_VELOCITY_LIMIT SQUARE_CORNER_VELOCITY=5
M204 S12000
SET_VELOCITY_LIMIT ACCEL_TO_DECEL=6000
M220 S100
M221 S100
G1 Z2.0 F1200
G1 X0.1 Y20 Z0.3 F6000.0
G1 X0.1 Y180.0 Z0.3 F3000.0 E10.0
G1 X0.4 Y180.0 Z0.3 F3000.0
G1 X0.4 Y20.0 Z0.3 F3000.0 E10.0
G1 Y10.0 F3000.0
G1 Z2.0 F3000.0
G92 E0
M82
G1 F12000
G21
{% endif %}
RESTORE_GCODE_STATE NAME=Prepurge_State
{% endif %}

View file

@ -0,0 +1,32 @@
###########################################
# PrusaSlicer Macros for Creality K1 Series
###########################################
[gcode_macro DEFINE_OBJECT]
gcode:
EXCLUDE_OBJECT_DEFINE {rawparams}
[gcode_macro START_CURRENT_OBJECT]
gcode:
EXCLUDE_OBJECT_START NAME={params.NAME}
[gcode_macro END_CURRENT_OBJECT]
gcode:
EXCLUDE_OBJECT_END {% if params.NAME %}NAME={params.NAME}{% endif %}
[gcode_macro LIST_OBJECTS]
gcode:
EXCLUDE_OBJECT_DEFINE
[gcode_macro LIST_EXCLUDED_OBJECTS]
gcode:
EXCLUDE_OBJECT
[gcode_macro REMOVE_ALL_EXCLUDED]
gcode:
EXCLUDE_OBJECT RESET=1

79
files/kamp/Smart_Park.cfg Normal file
View file

@ -0,0 +1,79 @@
###########################################
# Smart Park for Creality K1 Series
###########################################
[gcode_macro _SMART_PARK]
description: Parks your printhead near the print area for pre-print hotend heating.
gcode:
{% set kamp_settings = printer["gcode_macro _KAMP_Settings"] %}
{% set bed_x_max = printer["gcode_macro PRINTER_PARAM"].max_x_position | float %}
{% set bed_y_max = printer["gcode_macro PRINTER_PARAM"].max_y_position | float %}
{% set z_height = kamp_settings.smart_park_height | float %}
{% set purge_margin = kamp_settings.purge_margin | float %}
{% set purge_amount = kamp_settings.purge_amount | float %}
{% set verbose_enable = kamp_settings.verbose_enable | abs %}
{% set center_x = bed_x_max / 2 %}
{% set center_y = bed_y_max / 2 %}
{% set axis_minimum_x = printer.toolhead.axis_minimum.x | float %}
{% set axis_minimum_y = printer.toolhead.axis_minimum.y | float %}
{% set all_points = printer.exclude_object.objects | map(attribute='polygon') | sum(start=[]) %}
{% set x_min = (all_points | map(attribute=0) | min | default(0)) %}
{% set x_max = (all_points | map(attribute=0) | max | default(0)) %}
{% set y_min = (all_points | map(attribute=1) | min | default(0)) %}
{% set y_max = (all_points | map(attribute=1) | max | default(0)) %}
{% set travel_speed = (printer.toolhead.max_velocity) * 60 | float %}
{% set rapid_move = 10 %}
{% set park_x_center = ([((x_max + x_min) / 2) - (purge_amount / 2), 0] | max) %}
{% set park_y_center = ([((y_max + y_min) / 2) - (purge_amount / 2), 0] | max) %}
{% if (park_x_center + purge_amount + rapid_move) > bed_x_max %}
{% set park_x_center = (bed_x_max - (purge_amount + rapid_move)) %}
{% endif %}
{% if (park_y_center + purge_amount + rapid_move) > bed_y_max %}
{% set park_y_center = (bed_y_max - (purge_amount + rapid_move)) %}
{% endif %}
{% set park_x_origin_low = (x_min - purge_margin) %}
{% set park_x_origin_high = (x_max + purge_margin) %}
{% set park_y_origin_low = (y_min - purge_margin) %}
{% set park_y_origin_high = (y_max + purge_margin) %}
{% set detect_object = (x_min + x_max + y_min + y_max) %}
{% if detect_object == 0 %}
{% set x_min = 10 %}
{% set y_min = 10 %}
{% set z_height = 2 %}
{% elif park_y_origin_low > 0 %}
{% set x_min = park_x_center %}
{% set y_min = park_y_origin_low %}
{% elif park_x_origin_low > 0 %}
{% set x_min = park_x_origin_low %}
{% set y_min = park_y_center %}
{% elif park_y_origin_high < bed_y_max %}
{% set x_min = park_x_center %}
{% set y_min = park_y_origin_high %}
{% elif park_x_origin_high < bed_x_max %}
{% set x_min = park_x_origin_high %}
{% set y_min = park_y_center %}
{% else %}
{% set x_min = 10 %}
{% set y_min = 10 %}
{% set z_height = 2 %}
{% endif %}
{% if verbose_enable == True %}
RESPOND TYPE=command MSG="Smart Park location: {x_min},{y_min}"
{% endif %}
SAVE_GCODE_STATE NAME=Presmartpark_State
G90
{% if printer.toolhead.position.z < z_height %}
G0 Z{z_height}
{% endif %}
G0 X{x_min} Y{y_min} F{travel_speed}
G0 Z{z_height}
RESTORE_GCODE_STATE NAME=Presmartpark_State

View file

@ -0,0 +1,62 @@
###########################################
# Start Print Macro for Creality K1 Series
###########################################
[respond]
[virtual_pins]
[output_pin KAMP]
pin: virtual_pin:KAMP_pin
value: 1
[output_pin BED_LEVELING]
pin: virtual_pin:BED_LEVELING_pin
value: 1
[gcode_macro START_PRINT]
variable_prepare: 0
gcode:
WAIT_TEMP_END
CLEAR_PAUSE
{% set g28_extruder_temp = printer.custom_macro.g28_ext_temp %}
{% set bed_temp = printer.custom_macro.default_bed_temp %}
{% set extruder_temp = printer.custom_macro.default_extruder_temp %}
{% if 'BED_TEMP' in params|upper and (params.BED_TEMP|float) %}
{% set bed_temp = params.BED_TEMP %}
{% endif %}
{% if 'EXTRUDER_TEMP' in params|upper and (params.EXTRUDER_TEMP|float) %}
{% set extruder_temp = params.EXTRUDER_TEMP %}
{% endif %}
{% if printer['gcode_macro START_PRINT'].prepare|int == 0 %}
PRINT_PREPARE_CLEAR
CX_ROUGH_G28 EXTRUDER_TEMP={extruder_temp} BED_TEMP={bed_temp}
CX_NOZZLE_CLEAR
ACCURATE_G28
{% if printer.exclude_object.objects != [] and printer['output_pin KAMP'].value == 1 %}
RESPOND TYPE=command MSG="Starting KAMP Bed Mesh..."
BED_MESH_CLEAR
BED_MESH_CALIBRATE PROFILE=kamp
BED_MESH_PROFILE LOAD="kamp"
{% else %}
{% if printer['output_pin BED_LEVELING'].value == 1 %}
RESPOND TYPE=command MSG="Starting Full Bed Mesh..."
CX_PRINT_LEVELING_CALIBRATION
{% endif %}
BED_MESH_PROFILE LOAD="default"
{% endif %}
{% else %}
PRINT_PREPARE_CLEAR
{% endif %}
{% if printer.exclude_object.objects != [] and printer['output_pin KAMP'].value == 1 %}
_SMART_PARK
M109 S{extruder_temp}
M190 S{bed_temp}
RESPOND TYPE=command MSG="Starting KAMP line purge..."
_LINE_PURGE
{% else %}
RESPOND TYPE=command MSG="Starting classic line purge..."
CX_PRINT_DRAW_ONE_LINE
{% endif %}
SET_VELOCITY_LIMIT ACCEL={printer.configfile.settings.printer.max_accel}

View file

@ -0,0 +1,246 @@
# Virtual Pins support
#
# Copyright (C) 2023 Pedro Lamas <pedrolamas@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
class VirtualPins:
def __init__(self, config):
self._printer = config.get_printer()
ppins = self._printer.lookup_object('pins')
ppins.register_chip('virtual_pin', self)
self._pins = {}
self._oid_count = 0
self._config_callbacks = []
self._printer.register_event_handler("klippy:connect",
self.handle_connect)
def handle_connect(self):
for cb in self._config_callbacks:
cb()
def setup_pin(self, pin_type, pin_params):
ppins = self._printer.lookup_object('pins')
name = pin_params['pin']
if name in self._pins:
return self._pins[name]
if pin_type == 'digital_out':
pin = DigitalOutVirtualPin(self, pin_params)
elif pin_type == 'pwm':
pin = PwmVirtualPin(self, pin_params)
elif pin_type == 'adc':
pin = AdcVirtualPin(self, pin_params)
elif pin_type == 'endstop':
pin = EndstopVirtualPin(self, pin_params)
else:
raise ppins.error("unable to create virtual pin of type %s" % (
pin_type,))
self._pins[name] = pin
return pin
def create_oid(self):
self._oid_count += 1
return self._oid_count - 1
def register_config_callback(self, cb):
self._config_callbacks.append(cb)
def add_config_cmd(self, cmd, is_init=False, on_restart=False):
pass
def get_query_slot(self, oid):
return 0
def seconds_to_clock(self, time):
return 0
def get_printer(self):
return self._printer
def register_response(self, cb, msg, oid=None):
pass
def alloc_command_queue(self):
pass
def lookup_command(self, msgformat, cq=None):
return VirtualCommand()
def lookup_query_command(self, msgformat, respformat, oid=None,
cq=None, is_async=False):
return VirtualCommandQuery(respformat, oid)
def get_enumerations(self):
return {}
def print_time_to_clock(self, print_time):
return 0
def estimated_print_time(self, eventtime):
return 0
def register_stepqueue(self, stepqueue):
pass
def request_move_queue_slot(self):
pass
def get_status(self, eventtime):
return {
'pins': {
name : pin.get_status(eventtime)
for name, pin in self._pins.items()
}
}
class VirtualCommand:
def send(self, data=(), minclock=0, reqclock=0):
pass
def get_command_tag(self):
pass
class VirtualCommandQuery:
def __init__(self, respformat, oid):
entries = respformat.split()
self._response = {}
for entry in entries[1:]:
key, _ = entry.split('=')
self._response[key] = oid if key == 'oid' else 1
def send(self, data=(), minclock=0, reqclock=0):
return self._response
def send_with_preface(self, preface_cmd, preface_data=(), data=(),
minclock=0, reqclock=0):
return self._response
class VirtualPin:
def __init__(self, mcu, pin_params):
self._mcu = mcu
self._name = pin_params['pin']
self._pullup = pin_params['pullup']
self._invert = pin_params['invert']
self._value = self._pullup
printer = self._mcu.get_printer()
self._real_mcu = printer.lookup_object('mcu')
gcode = printer.lookup_object('gcode')
gcode.register_mux_command("SET_VIRTUAL_PIN", "PIN", self._name,
self.cmd_SET_VIRTUAL_PIN,
desc=self.cmd_SET_VIRTUAL_PIN_help)
cmd_SET_VIRTUAL_PIN_help = "Set the value of an output pin"
def cmd_SET_VIRTUAL_PIN(self, gcmd):
self._value = gcmd.get_float('VALUE', minval=0., maxval=1.)
def get_mcu(self):
return self._real_mcu
class DigitalOutVirtualPin(VirtualPin):
def __init__(self, mcu, pin_params):
VirtualPin.__init__(self, mcu, pin_params)
def setup_max_duration(self, max_duration):
pass
def setup_start_value(self, start_value, shutdown_value):
self._value = start_value
def set_digital(self, print_time, value):
self._value = value
def get_status(self, eventtime):
return {
'value': self._value,
'type': 'digital_out'
}
class PwmVirtualPin(VirtualPin):
def __init__(self, mcu, pin_params):
VirtualPin.__init__(self, mcu, pin_params)
def setup_max_duration(self, max_duration):
pass
def setup_start_value(self, start_value, shutdown_value):
self._value = start_value
def setup_cycle_time(self, cycle_time, hardware_pwm=False):
pass
def set_pwm(self, print_time, value, cycle_time=None):
self._value = value
def get_status(self, eventtime):
return {
'value': self._value,
'type': 'pwm'
}
class AdcVirtualPin(VirtualPin):
def __init__(self, mcu, pin_params):
VirtualPin.__init__(self, mcu, pin_params)
self._callback = None
self._min_sample = 0.
self._max_sample = 0.
printer = self._mcu.get_printer()
printer.register_event_handler("klippy:connect",
self.handle_connect)
def handle_connect(self):
reactor = self._mcu.get_printer().get_reactor()
reactor.register_timer(self._raise_callback, reactor.monotonic() + 2.)
def setup_adc_callback(self, report_time, callback):
self._callback = callback
def setup_minmax(self, sample_time, sample_count,
minval=0., maxval=1., range_check_count=0):
self._min_sample = minval
self._max_sample = maxval
def _raise_callback(self, eventtime):
range = self._max_sample - self._min_sample
sample_value = (self._value * range) + self._min_sample
self._callback(eventtime, sample_value)
return eventtime + 2.
def get_status(self, eventtime):
return {
'value': self._value,
'type': 'adc'
}
class EndstopVirtualPin(VirtualPin):
def __init__(self, mcu, pin_params):
VirtualPin.__init__(self, mcu, pin_params)
self._steppers = []
def add_stepper(self, stepper):
self._steppers.append(stepper)
def query_endstop(self, print_time):
return self._value
def home_start(self, print_time, sample_time, sample_count, rest_time,
triggered=True):
reactor = self._mcu.get_printer().get_reactor()
completion = reactor.completion()
completion.complete(True)
return completion
def home_wait(self, home_end_time):
return 1
def get_steppers(self):
return list(self._steppers)
def get_status(self, eventtime):
return {
'value': self._value,
'type': 'endstop'
}
def load_config(config):
return VirtualPins(config)

View file

@ -0,0 +1,133 @@
########################################
# M600 Support
########################################
[respond]
[idle_timeout]
gcode:
RESPOND TYPE=command MSG="Stopping hotend heating..."
M104 S0
timeout: 99999999
[filament_switch_sensor filament_sensor]
pause_on_runout: true
switch_pin: !PC15
runout_gcode:
M600
[gcode_macro M600]
description: Filament Change
variable_m600_state: 0
gcode:
{% set E = printer["gcode_macro PAUSE"].extrude|float %}
{% set y_park = printer.toolhead.axis_minimum.y|float + 5.0 %}
{% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %}
{% set max_z = printer["gcode_macro PRINTER_PARAM"].max_z_position|float %}
{% set act_z = printer.toolhead.position.z|float %}
{% set z_safe = 0.0 %}
{% if act_z < 48.0 %}
{% set z_safe = 50.0 - act_z %}
{% elif act_z < (max_z - 2.0) %}
{% set z_safe = 2.0 %}
{% elif act_z < max_z %}
{% set z_safe = max_z - act_z %}
{% endif %}
{action_respond_info("z_safe = %s"% (z_safe))}
SET_GCODE_VARIABLE MACRO=M600 VARIABLE=m600_state VALUE=1
SET_GCODE_VARIABLE MACRO=PRINTER_PARAM VARIABLE=hotend_temp VALUE={printer.extruder.target}
RESPOND TYPE=command MSG="Print paused for filament change!"
PAUSE_BASE
G91
{% if "xyz" in printer.toolhead.homed_axes %}
{% if printer.extruder.can_extrude|lower == 'true' %}
G1 E-1.0 F180
G1 E-{E} F4000
{% else %}
RESPOND TYPE=command MSG="Extruder not hot enough!"
{% endif %}
G1 Z{z_safe} F600
M400
G90
G1 X{x_park} Y{y_park} F30000
{% endif %}
G91
{% if printer.extruder.can_extrude|lower == 'true' %}
RESPOND TYPE=command MSG="Extracting filament..."
G1 E20 F180
G1 E-30 F180
G1 E-50 F2000
{% else %}
RESPOND TYPE=command MSG="Extruder not hot enough!"
{% endif %}
SET_GCODE_VARIABLE MACRO=PRINTER_PARAM VARIABLE=fan2_speed VALUE={printer['output_pin fan2'].value}
SET_GCODE_VARIABLE MACRO=PRINTER_PARAM VARIABLE=fan0_speed VALUE={printer['output_pin fan0'].value}
M106 P0 S0
M106 P2 S0
SET_IDLE_TIMEOUT TIMEOUT=900
SET_E_MIN_CURRENT
RESPOND TYPE=command MSG="Replace filament at the extruder inlet and click on Resume button!"
[gcode_macro RESUME]
description: Resume the current print
rename_existing: RESUME_BASE
gcode:
RESTORE_E_CURRENT
{% if printer['gcode_macro PRINTER_PARAM'].hotend_temp|int != 0 %}
{% if printer['gcode_macro PRINTER_PARAM'].hotend_temp|int > printer.extruder.temperature %}
RESPOND TYPE=command MSG="Starting hotend heating..."
M109 S{printer['gcode_macro PRINTER_PARAM'].hotend_temp|int}
{% else %}
RESPOND TYPE=command MSG="Starting hotend heating..."
M104 S{printer['gcode_macro PRINTER_PARAM'].hotend_temp|int}
{% endif %}
SET_GCODE_VARIABLE MACRO=PRINTER_PARAM VARIABLE=hotend_temp VALUE=0
{% endif %}
{% if printer['gcode_macro PRINTER_PARAM'].fan2_speed > 0 %}
{% set s_value = (printer['gcode_macro PRINTER_PARAM'].fan2_speed * 255 - printer['gcode_macro PRINTER_PARAM'].fan2_min) * 255 / (255 - printer['gcode_macro PRINTER_PARAM'].fan2_min)|float %}
M106 P2 S{s_value}
{% endif %}
{% set z_resume_move = printer['gcode_macro PRINTER_PARAM'].z_safe_pause|int %}
{% if z_resume_move > 2 %}
{% set z_resume_move = z_resume_move - 2 %}
G91
G1 Z-{z_resume_move} F600
M400
{% endif %}
{% set E = printer["gcode_macro PAUSE"].extrude|float + 1.0 %}
{% if 'VELOCITY' in params|upper %}
{% set get_params = ('VELOCITY=' + params.VELOCITY) %}
{%else %}
{% set get_params = "" %}
{% endif %}
{% if printer["gcode_macro M600"].m600_state == 1 %}
{% if printer.extruder.can_extrude|lower == 'true' %}
RESPOND TYPE=command MSG="Loading and purging filament..."
G91
G1 E180 F180
G90
M400
{% else %}
RESPOND TYPE=command MSG="Extruder not hot enough!"
{% endif %}
{% if printer['gcode_macro PRINTER_PARAM'].fan0_speed > 0 %}
{% set s_value = (printer['gcode_macro PRINTER_PARAM'].fan0_speed * 255 - printer['gcode_macro PRINTER_PARAM'].fan0_min) * 255 / (255 - printer['gcode_macro PRINTER_PARAM'].fan0_min)|float %}
M106 P0 S{s_value}
{% endif %}
SET_GCODE_VARIABLE MACRO=M600 VARIABLE=m600_state VALUE=0
SET_IDLE_TIMEOUT TIMEOUT=99999999
{% else %}
{% if printer.extruder.can_extrude|lower == 'true' %}
G91
G1 E{E} F2100
G90
M400
{% else %}
RESPOND TYPE=command MSG="Extruder not hot enough!"
{% endif %}
{% endif %}
RESPOND TYPE=command MSG="Restarting print..."
RESUME_BASE {get_params}

View file

@ -0,0 +1,142 @@
########################################
# Fans Control
########################################
[respond]
[duplicate_pin_override]
pins: PC0, PC5, PB2, ADC_TEMPERATURE
[temperature_fan chamber_fan]
pin: PC0
cycle_time: 0.0100
hardware_pwm: false
max_power: 1
shutdown_speed: 0
sensor_type: EPCOS 100K B57560G104F
sensor_pin: PC5
min_temp: 0
max_temp: 70
control: watermark
max_delta: 2
target_temp: 35.0
max_speed: 1.0
min_speed: 0.0
[temperature_fan mcu_fan]
pin: PB2
cycle_time: 0.0100
hardware_pwm: false
max_power: 1
shutdown_speed: 0
sensor_type: temperature_mcu
min_temp: 0
max_temp: 100
control: watermark
max_delta: 2
target_temp: 50.0
max_speed: 1.0
min_speed: 0.0
[output_pin mcu_fan]
pin: PB2
pwm: True
cycle_time: 0.0100
hardware_pwm: false
value: 0.00
scale: 255
shutdown_value: 0.0
[gcode_macro M141]
description: Set Chamber Temperature with slicers
gcode:
{% set s = params.S|float %}
SET_TEMPERATURE_FAN_TARGET TEMPERATURE_FAN=chamber_fan TARGET={s}
RESPOND TYPE=command MSG="Chamber target temperature: {s}°C"
[gcode_macro M191]
description: Wait for Chamber Temperature to heat up
gcode:
{% set s = params.S|float %}
{% set chamber_temp = printer["temperature_sensor chamber_temp"].temperature|float %}
{% if s > 0 %}
M141 S{s}
{% endif %}
{% if s > chamber_temp and s <= 90 %}
M140 S100
RESPOND TYPE=command MSG="Waiting for the bed to heat up the chamber..."
TEMPERATURE_WAIT SENSOR="temperature_fan chamber_fan" MINIMUM={s-1}
RESPOND TYPE=command MSG="Chamber target temperature reached: {s}°C"
M140 S{s}
{% endif %}
[gcode_macro M106]
gcode:
{% set fans = printer["gcode_macro PRINTER_PARAM"].fans|int %}
{% set fan = 0 %}
{% set value = 0 %}
{% if params.P is defined %}
{% set tmp = params.P|int %}
{% if tmp < fans %}
{% set fan = tmp %}
{% endif %}
{% endif %}
{% if params.S is defined %}
{% set tmp = params.S|float %}
{% else %}
{% set tmp = 255 %}
{% endif %}
{% if tmp > 0 %}
{% if fan == 0 %}
{% set value = (255 - printer["gcode_macro PRINTER_PARAM"].fan0_min) / 255 * tmp %}
{% if printer['gcode_macro Qmode'].flag | int == 1 %}
SET_GCODE_VARIABLE MACRO=Qmode VARIABLE=fan0_value VALUE={printer["gcode_macro PRINTER_PARAM"].fan0_min + value}
{% if value > (255 - printer['gcode_macro PRINTER_PARAM'].fan0_min) / 2 %}
{% set value = printer["gcode_macro PRINTER_PARAM"].fan0_min + (255 - printer['gcode_macro PRINTER_PARAM'].fan0_min) / 2 %}
{% else %}
{% set value = printer["gcode_macro PRINTER_PARAM"].fan0_min + value %}
{% endif %}
{% else %}
{% set value = printer["gcode_macro PRINTER_PARAM"].fan0_min + value %}
{% endif %}
{% endif %}
{% if fan == 1 %}
{% set value = (255 - printer["gcode_macro PRINTER_PARAM"].fan1_min) / 255 * tmp %}
{% if printer['gcode_macro Qmode'].flag | int == 1 %}
SET_GCODE_VARIABLE MACRO=Qmode VARIABLE=fan1_value VALUE={printer["gcode_macro PRINTER_PARAM"].fan1_min + value}
{% if value > (255 - printer['gcode_macro PRINTER_PARAM'].fan1_min) / 2 %}
{% set value = printer["gcode_macro PRINTER_PARAM"].fan1_min + (255 - printer['gcode_macro PRINTER_PARAM'].fan1_min) / 2 %}
{% else %}
{% set value = printer["gcode_macro PRINTER_PARAM"].fan1_min + value %}
{% endif %}
{% else %}
{% set value = printer["gcode_macro PRINTER_PARAM"].fan1_min + value %}
{% endif %}
{% endif %}
{% if fan == 2 %}
{% set value = (255 - printer["gcode_macro PRINTER_PARAM"].fan2_min) / 255 * tmp %}
{% if printer['gcode_macro Qmode'].flag | int == 1 %}
SET_GCODE_VARIABLE MACRO=Qmode VARIABLE=fan2_value VALUE={printer["gcode_macro PRINTER_PARAM"].fan2_min + value}
{% if value > (255 - printer['gcode_macro PRINTER_PARAM'].fan2_min) / 2 %}
{% set value = printer["gcode_macro PRINTER_PARAM"].fan2_min + (255 - printer['gcode_macro PRINTER_PARAM'].fan2_min) / 2 %}
{% else %}
{% set value = printer["gcode_macro PRINTER_PARAM"].fan2_min + value %}
{% endif %}
{% else %}
{% set value = printer["gcode_macro PRINTER_PARAM"].fan2_min + value %}
{% endif %}
{% endif %}
{% endif %}
{% if value >= 255 %}
{% set value = 255 %}
{% endif %}
{% if params.P is defined and params.P|int == 3 %}
{% set fan = 1 %}
{% endif %}
SET_PIN PIN=fan{fan} VALUE={value}

View file

@ -0,0 +1,38 @@
########################################
# Save Z-Offset
########################################
[save_variables]
filename: /usr/data/printer_data/config/Helper-Script/variables.cfg
[respond]
[gcode_macro SET_GCODE_OFFSET]
description: Saving Z-Offset
rename_existing: _SET_GCODE_OFFSET
gcode:
{% if printer.save_variables.variables.zoffset %}
{% set zoffset = printer.save_variables.variables.zoffset %}
{% else %}
{% set zoffset = {'z': None} %}
{% endif %}
{% set ns = namespace(zoffset={'z': zoffset.z}) %}
_SET_GCODE_OFFSET {% for p in params %}{'%s=%s '% (p, params[p])}{% endfor %}
{%if 'Z' in params %}{% set null = ns.zoffset.update({'z': params.Z}) %}{% endif %}
{%if 'Z_ADJUST' in params %}
{%if ns.zoffset.z == None %}{% set null = ns.zoffset.update({'z': 0}) %}{% endif %}
{% set null = ns.zoffset.update({'z': (ns.zoffset.z | float) + (params.Z_ADJUST | float)}) %}
{% endif %}
SAVE_VARIABLE VARIABLE=zoffset VALUE="{ns.zoffset}"
[delayed_gcode LOAD_GCODE_OFFSETS]
initial_duration: 2
gcode:
{% if printer.save_variables.variables.zoffset %}
{% set zoffset = printer.save_variables.variables.zoffset %}
_SET_GCODE_OFFSET {% for axis, offset in zoffset.items() if zoffset[axis] %}{ "%s=%s " % (axis, offset) }{% endfor %}
RESPOND TYPE=command MSG="Loaded Z-Offset from variables.cfg: {zoffset.z}mm"
{% endif %}

View file

@ -0,0 +1,205 @@
########################################
# Useful Macros
########################################
[gcode_shell_command Klipper_Backup]
command: sh /usr/data/helper-script/files/scripts/useful_macros.sh -backup_klipper
timeout: 600.0
verbose: true
[gcode_shell_command Klipper_Restore]
command: sh /usr/data/helper-script/files/scripts/useful_macros.sh -restore_klipper
timeout: 600.0
verbose: true
[gcode_shell_command Moonraker_Backup]
command: sh /usr/data/helper-script/files/scripts/useful_macros.sh -backup_moonraker
timeout: 600.0
verbose: true
[gcode_shell_command Moonraker_Restore]
command: sh /usr/data/helper-script/files/scripts/useful_macros.sh -restore_moonraker
timeout: 600.0
verbose: true
[gcode_macro KLIPPER_BACKUP_CONFIG]
gcode:
RUN_SHELL_COMMAND CMD=Klipper_Backup
[gcode_macro KLIPPER_RESTORE_CONFIG]
gcode:
RUN_SHELL_COMMAND CMD=Klipper_Restore
[gcode_macro MOONRAKER_BACKUP_DATABASE]
gcode:
RUN_SHELL_COMMAND CMD=Moonraker_Backup
[gcode_macro MOONRAKER_RESTORE_DATABASE]
gcode:
RUN_SHELL_COMMAND CMD=Moonraker_Restore
[gcode_macro BED_LEVELING]
description: Start Bed Leveling
gcode:
{% if 'PROBE_COUNT' in params|upper %}
{% set get_count = ('PROBE_COUNT=' + params.PROBE_COUNT) %}
{%else %}
{% set get_count = "" %}
{% endif %}
{% set bed_temp = params.BED_TEMP|default(50)|float %}
{% set hotend_temp = params.HOTEND_TEMP|default(140)|float %}
{% set nozzle_clear_temp = params.NOZZLE_CLEAR_TEMP|default(240)|float %}
SET_FILAMENT_SENSOR SENSOR=filament_sensor ENABLE=0
{% if printer.toolhead.homed_axes != "xyz" %}
G28
{% endif %}
BED_MESH_CLEAR
NOZZLE_CLEAR HOT_MIN_TEMP={hotend_temp} HOT_MAX_TEMP={nozzle_clear_temp} BED_MAX_TEMP={bed_temp}
ACCURATE_G28
M204 S5000
SET_VELOCITY_LIMIT ACCEL_TO_DECEL=5000
BED_MESH_CALIBRATE {get_count}
BED_MESH_OUTPUT
{% set y_park = printer.toolhead.axis_maximum.y/2 %}
{% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %}
G1 X{x_park} Y{y_park} F20000
TURN_OFF_HEATERS
SET_FILAMENT_SENSOR SENSOR=filament_sensor ENABLE=1
[gcode_macro PID_BED]
description: Start Bed PID
gcode:
G90
{% if printer.toolhead.homed_axes != "xyz" %}
G28
{% endif %}
G1 Z10 F600
M106
PID_CALIBRATE HEATER=heater_bed TARGET={params.BED_TEMP|default(70)}
M107
{% set y_park = printer.toolhead.axis_maximum.y/2 %}
{% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %}
G1 X{x_park} Y{y_park} F20000
[gcode_macro PID_HOTEND]
description: Start Hotend PID
gcode:
G90
{% if printer.toolhead.homed_axes != "xyz" %}
G28
{% endif %}
G1 Z10 F600
M106
PID_CALIBRATE HEATER=extruder TARGET={params.HOTEND_TEMP|default(250)}
M107
{% set y_park = printer.toolhead.axis_maximum.y/2 %}
{% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %}
G1 X{x_park} Y{y_park} F20000
WAIT_TEMP_START
[gcode_macro LUBRICATE_RODS]
description: Distribute lubricant on Rods
gcode:
{% set min_speed = 3000 %} # Minimum speed in mm/min
{% set max_speed = 18000 %} # Maximum speed in mm/min
{% if printer.toolhead.homed_axes != "xyz" %}
G28
{% endif %}
G1 Z50 F300
{% set x_max = printer.toolhead.axis_maximum.x|int %}
{% set y_max = printer.toolhead.axis_maximum.y|int %}
{% set edge_offset_x = x_max * 0.05 %}
{% set edge_offset_y = y_max * 0.05 %}
{% set x_range = x_max - edge_offset_x %}
{% set y_range = y_max - edge_offset_y %}
{% set num_steps_x = (x_range / 10)|int %}
{% set num_steps_y = (y_range / 10)|int %}
{% set speed_increment_x = (max_speed - min_speed) / num_steps_x %}
{% set speed_increment_y = (max_speed - min_speed) / num_steps_y %}
{% set current_speed_x = min_speed %}
{% set current_speed_y = min_speed %}
{% for i in range(num_steps_x) %}
G1 X{edge_offset_x + i * 10} Y{edge_offset_y} F{current_speed_x}
G1 X{edge_offset_x + i * 10} Y{y_range} F{current_speed_x}
{% set current_speed_x = current_speed_x + speed_increment_x %}
{% endfor %}
{% for j in range(num_steps_y) %}
G1 Y{edge_offset_y + j * 10} X{edge_offset_x} F{current_speed_y}
G1 Y{edge_offset_y + j * 10} X{x_range} F{current_speed_y}
{% set current_speed_y = current_speed_y + speed_increment_y %}
{% endfor %}
[gcode_macro WARMUP]
description: Stress Test
variable_maxd: 14142.14 ; = SQRT(2*maxy)
gcode:
{% set min_loops = 2 %}
{% set max_loops = params.LOOPS|default(3)|int %}
{% if 'LOOPS' in params|upper %}
{% if max_loops < min_loops %}
{% set max_loops = min_loops %}
{% endif %}
{% endif %}
{% set loop_cnt = max_loops %}
{% if 'X_ACCEL_MAX' in params|upper %}
{% set maxx = params.X_ACCEL_MAX|default(10000)|int %}
{% endif %}
{% if 'Y_ACCEL_MAX' in params|upper %}
{% set maxy = params.Y_ACCEL_MAX|default(10000)|int %}
{% endif %}
{% set max_x = (printer.toolhead.axis_maximum.x|int-5) %}
{% set max_y = (printer.toolhead.axis_maximum.y|int-5) %}
{% set loop_step_y = max_y//(loop_cnt-1) %}
{% set loop_step_x = max_x//(loop_cnt-1) %}
{% set y_park = printer.toolhead.axis_maximum.y/2 %}
{% set x_park = printer.toolhead.axis_maximum.x|float - 10.0 %}
{% if printer.toolhead.homed_axes != "xyz" %}
G28
{% endif %}
SET_VELOCITY_LIMIT ACCEL={maxx} ACCEL_TO_DECEL={maxx/2}
{% for number in range(10,max_y+11,loop_step_y) %}
{% if number >= max_y %}
{% set number = max_y %}
{% endif %}
G1 F{maxy} X10 Y{number}
G1 F{maxx} X{max_x} Y{number}
{% endfor %}
SET_VELOCITY_LIMIT ACCEL={maxy} ACCEL_TO_DECEL={maxy/2}
{% for number in range(10,max_x+11,loop_step_y) %}
{% if number >= max_x %}
{% set number = max_x %}
{% endif %}
G1 F{maxy} X{number} Y{max_y}
G1 F{maxy} X{number} Y10
{% endfor %}
SET_VELOCITY_LIMIT ACCEL={maxd} ACCEL_TO_DECEL={maxd/2}
{% for times in range(loop_cnt) %}
G1 F{maxx} X10 Y10
G1 F{maxd} X{max_x} Y{max_y}
G1 F{maxx} X10 Y{max_y}
G1 F{maxd} X{max_x} Y10
G1 F{maxy} X{max_x} Y{max_y}
G1 F{maxd} X10 Y10
G1 F{maxy} X10 Y{max_y}
G1 F{maxd} X{max_x} Y10
{% endfor %}
SET_VELOCITY_LIMIT ACCEL={maxx} ACCEL_TO_DECEL={maxx/2}
{% for times in range(loop_cnt) %}
G1 F{maxy} X10 Y10
G1 F{maxy} X10 Y{max_y}
G1 F{maxx} X{max_x} Y{max_y}
G1 F{maxy} X{max_x} Y10
G1 F{maxx} X10 Y10
G1 F{maxx} X{max_x} Y10
G1 F{maxy} X{max_x} Y{max_y}
G1 F{maxx} X10 Y{max_y}
{% endfor %}
G1 X{x_park} Y{y_park} F30000

View file

@ -0,0 +1,427 @@
# Timelapse klipper macro definition
#
# Copyright (C) 2021 Christoph Frei <fryakatkop@gmail.com>
# Copyright (C) 2021 Alex Zellner <alexander.zellner@googlemail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license
#
# Macro version 1.15
#
##### DO NOT CHANGE ANY MACRO!!! #####
##########################################################################
# #
# GET_TIMELAPSE_SETUP: Print the Timelapse setup to console #
# #
##########################################################################
[gcode_macro GET_TIMELAPSE_SETUP]
description: Print the Timelapse setup
gcode:
{% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %}
{% set output_txt = ["Timelapse Setup:"] %}
{% set _dummy = output_txt.append("enable: %s" % tl.enable) %}
{% set _dummy = output_txt.append("park: %s" % tl.park.enable) %}
{% if tl.park.enable %}
{% set _dummy = output_txt.append("park position: %s time: %s s" % (tl.park.pos, tl.park.time)) %}
{% set _dummy = output_txt.append("park cord x:%s y:%s dz:%s" % (tl.park.coord.x, tl.park.coord.y, tl.park.coord.dz)) %}
{% set _dummy = output_txt.append("travel speed: %s mm/s" % tl.speed.travel) %}
{% endif %}
{% set _dummy = output_txt.append("fw_retract: %s" % tl.extruder.fw_retract) %}
{% if not tl.extruder.fw_retract %}
{% set _dummy = output_txt.append("retract: %s mm speed: %s mm/s" % (tl.extruder.retract, tl.speed.retract)) %}
{% set _dummy = output_txt.append("extrude: %s mm speed: %s mm/s" % (tl.extruder.extrude, tl.speed.extrude)) %}
{% endif %}
{% set _dummy = output_txt.append("verbose: %s" % tl.verbose) %}
{action_respond_info(output_txt|join("\n"))}
################################################################################################
# #
# Use _SET_TIMELAPSE_SETUP [ENABLE=value] [VERBOSE=value] [PARK_ENABLE=value] [PARK_POS=value] #
# [PARK_TIME=value] [CUSTOM_POS_X=value] [CUSTOM_POS_Y=value] #
# [CUSTOM_POS_DZ=value][TRAVEL_SPEED=value] [RETRACT_SPEED=value] #
# [EXTRUDE_SPEED=value] [EXTRUDE_DISTANCE=value] #
# [RETRACT_DISTANCE=value] [FW_RETRACT=value] #
# #
################################################################################################
[gcode_macro _SET_TIMELAPSE_SETUP]
description: Set user parameters for timelapse
gcode:
{% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %}
##### get min and max bed size #####
{% set min = printer.toolhead.axis_minimum %}
{% set max = printer.toolhead.axis_maximum %}
{% set round_bed = True if printer.configfile.settings.printer.kinematics is in ['delta','polar','rotary_delta','winch']
else False %}
{% set park = {'min' : {'x': (min.x / 1.42)|round(3) if round_bed else min.x|round(3),
'y': (min.y / 1.42)|round(3) if round_bed else min.y|round(3)},
'max' : {'x': (max.x / 1.42)|round(3) if round_bed else max.x|round(3),
'y': (max.y / 1.42)|round(3) if round_bed else max.y|round(3)},
'center': {'x': (max.x-(max.x-min.x)/2)|round(3),
'y': (max.y-(max.y-min.y)/2)|round(3)}} %}
##### set new values #####
{% if params.ENABLE %}
{% if params.ENABLE|lower is in ['true', 'false'] %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=enable VALUE={True if params.ENABLE|lower == 'true' else False}
{% else %}
{action_raise_error("ENABLE=%s not supported. Allowed values are [True, False]" % params.ENABLE|capitalize)}
{% endif %}
{% endif %}
{% if params.VERBOSE %}
{% if params.VERBOSE|lower is in ['true', 'false'] %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=verbose VALUE={True if params.VERBOSE|lower == 'true' else False}
{% else %}
{action_raise_error("VERBOSE=%s not supported. Allowed values are [True, False]" % params.VERBOSE|capitalize)}
{% endif %}
{% endif %}
{% if params.CUSTOM_POS_X %}
{% if params.CUSTOM_POS_X|float >= min.x and params.CUSTOM_POS_X|float <= max.x %}
{% set _dummy = tl.park.custom.update({'x':params.CUSTOM_POS_X|float|round(3)}) %}
{% else %}
{action_raise_error("CUSTOM_POS_X=%s must be within [%s - %s]" % (params.CUSTOM_POS_X, min.x, max.x))}
{% endif %}
{% endif %}
{% if params.CUSTOM_POS_Y %}
{% if params.CUSTOM_POS_Y|float >= min.y and params.CUSTOM_POS_Y|float <= max.y %}
{% set _dummy = tl.park.custom.update({'y':params.CUSTOM_POS_Y|float|round(3)}) %}
{% else %}
{action_raise_error("CUSTOM_POS_Y=%s must be within [%s - %s]" % (params.CUSTOM_POS_Y, min.y, max.y))}
{% endif %}
{% endif %}
{% if params.CUSTOM_POS_DZ %}
{% if params.CUSTOM_POS_DZ|float >= min.z and params.CUSTOM_POS_DZ|float <= max.z %}
{% set _dummy = tl.park.custom.update({'dz':params.CUSTOM_POS_DZ|float|round(3)}) %}
{% else %}
{action_raise_error("CUSTOM_POS_DZ=%s must be within [%s - %s]" % (params.CUSTOM_POS_DZ, min.z, max.z))}
{% endif %}
{% endif %}
{% if params.PARK_ENABLE %}
{% if params.PARK_ENABLE|lower is in ['true', 'false'] %}
{% set _dummy = tl.park.update({'enable':True if params.PARK_ENABLE|lower == 'true' else False}) %}
{% else %}
{action_raise_error("PARK_ENABLE=%s not supported. Allowed values are [True, False]" % params.PARK_ENABLE|capitalize)}
{% endif %}
{% endif %}
{% if params.PARK_POS %}
{% if params.PARK_POS|lower is in ['center','front_left','front_right','back_left','back_right','custom','x_only','y_only'] %}
{% set dic = {'center' : {'x': park.center.x , 'y': park.center.y , 'dz': 1 },
'front_left' : {'x': park.min.x , 'y': park.min.y , 'dz': 0 },
'front_right' : {'x': park.max.x , 'y': park.min.y , 'dz': 0 },
'back_left' : {'x': park.min.x , 'y': park.max.y , 'dz': 0 },
'back_right' : {'x': park.max.x , 'y': park.max.y , 'dz': 0 },
'custom' : {'x': tl.park.custom.x, 'y': tl.park.custom.y, 'dz': tl.park.custom.dz},
'x_only' : {'x': tl.park.custom.x, 'y': 'none' , 'dz': tl.park.custom.dz},
'y_only' : {'x': 'none' , 'y': tl.park.custom.y, 'dz': tl.park.custom.dz}} %}
{% set _dummy = tl.park.update({'pos':params.PARK_POS|lower}) %}
{% set _dummy = tl.park.update({'coord':dic[tl.park.pos]}) %}
{% else %}
{action_raise_error("PARK_POS=%s not supported. Allowed values are [CENTER, FRONT_LEFT, FRONT_RIGHT, BACK_LEFT, BACK_RIGHT, CUSTOM, X_ONLY, Y_ONLY]"
% params.PARK_POS|upper)}
{% endif %}
{% endif %}
{% if params.PARK_TIME %}
{% if params.PARK_TIME|float >= 0.0 %}
{% set _dummy = tl.park.update({'time':params.PARK_TIME|float|round(3)}) %}
{% else %}
{action_raise_error("PARK_TIME=%s must be a positive number" % params.PARK_TIME)}
{% endif %}
{% endif %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=park VALUE="{tl.park}"
{% if params.TRAVEL_SPEED %}
{% if params.TRAVEL_SPEED|float > 0.0 %}
{% set _dummy = tl.speed.update({'travel':params.TRAVEL_SPEED|float|round(3)}) %}
{% else %}
{action_raise_error("TRAVEL_SPEED=%s must be larger than 0" % params.TRAVEL_SPEED)}
{% endif %}
{% endif %}
{% if params.RETRACT_SPEED %}
{% if params.RETRACT_SPEED|float > 0.0 %}
{% set _dummy = tl.speed.update({'retract':params.RETRACT_SPEED|float|round(3)}) %}
{% else %}
{action_raise_error("RETRACT_SPEED=%s must be larger than 0" % params.RETRACT_SPEED)}
{% endif %}
{% endif %}
{% if params.EXTRUDE_SPEED %}
{% if params.EXTRUDE_SPEED|float > 0.0 %}
{% set _dummy = tl.speed.update({'extrude':params.EXTRUDE_SPEED|float|round(3)}) %}
{% else %}
{action_raise_error("EXTRUDE_SPEED=%s must be larger than 0" % params.EXTRUDE_SPEED)}
{% endif %}
{% endif %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=speed VALUE="{tl.speed}"
{% if params.EXTRUDE_DISTANCE %}
{% if params.EXTRUDE_DISTANCE|float >= 0.0 %}
{% set _dummy = tl.extruder.update({'extrude':params.EXTRUDE_DISTANCE|float|round(3)}) %}
{% else %}
{action_raise_error("EXTRUDE_DISTANCE=%s must be specified as positiv number" % params.EXTRUDE_DISTANCE)}
{% endif %}
{% endif %}
{% if params.RETRACT_DISTANCE %}
{% if params.RETRACT_DISTANCE|float >= 0.0 %}
{% set _dummy = tl.extruder.update({'retract':params.RETRACT_DISTANCE|float|round(3)}) %}
{% else %}
{action_raise_error("RETRACT_DISTANCE=%s must be specified as positiv number" % params.RETRACT_DISTANCE)}
{% endif %}
{% endif %}
{% if params.FW_RETRACT %}
{% if params.FW_RETRACT|lower is in ['true', 'false'] %}
{% if 'firmware_retraction' in printer.configfile.settings %}
{% set _dummy = tl.extruder.update({'fw_retract': True if params.FW_RETRACT|lower == 'true' else False}) %}
{% else %}
{% set _dummy = tl.extruder.update({'fw_retract':False}) %}
{% if params.FW_RETRACT|capitalize == 'True' %}
{action_raise_error("[firmware_retraction] not defined in printer.cfg. Can not enable fw_retract")}
{% endif %}
{% endif %}
{% else %}
{action_raise_error("FW_RETRACT=%s not supported. Allowed values are [True, False]" % params.FW_RETRACT|capitalize)}
{% endif %}
{% endif %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=extruder VALUE="{tl.extruder}"
{% if printer.configfile.settings['gcode_macro pause'] is defined %}
{% set _dummy = tl.macro.update({'pause': printer.configfile.settings['gcode_macro pause'].rename_existing}) %}
{% endif %}
{% if printer.configfile.settings['gcode_macro resume'] is defined %}
{% set _dummy = tl.macro.update({'resume': printer.configfile.settings['gcode_macro resume'].rename_existing}) %}
{% endif %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=macro VALUE="{tl.macro}"
##########################################################################
# #
# TIMELAPSE_TAKE_FRAME: take the next picture #
# #
##########################################################################
######################### definition #########################
## enable: enable or disable the next frame. Valid inputs: [True, False]
## takingframe: internal use. Valid inputs: [True, False]
##
## park.enable: enable or disable to park the head while taking a picture. Valid inputs: [True, False]
## park.pos : used position for parking. Valid inputs: [center, front_left, front_right, back_left, back_right, custom, x_only, y_only]
## park.time : used for the debug macro. Time in s
## park.custom.x, park.custom.y: coordinates of the custom parkposition. Unit [mm]
## park.custom.dz : custom z hop for the picture. Unit [mm]
## park.coord : internal use
##
## extruder.fw_retract: enable disable fw retraction [True,False]
## extruder.extrude : filament extruded at the end of park. Unit [mm]
## extruder.retract : filament retract at the start of park. Unit [mm]
##
## speed.travel : used speed for travel from and to the park positon. Unit: [mm/min]
## speed.retract: used speed for retract [mm/min]
## speed.extrude: used speed for extrude [mm/min]
##
## verbose: Enable mesage output of TIMELAPSE_TAKE_FRAME
##
## check_time: time when the status of the taken picture is checked. Default 0.5 sec
##
## restore.absolute.coordinates: internal use
## restore.absolute.extrude : internal use
## restore.speed : internal use
## restore.e : internal use
## restore.factor.speed : internal use
## restore.factor.extrude : internal use
##
## macro.pause : internal use
## macro.resume : internal use
##
## is_paused: internal use
###############################################################
[gcode_macro TIMELAPSE_TAKE_FRAME]
description: Take Timelapse shoot
variable_enable: False
variable_takingframe: False
variable_park: {'enable': False,
'pos' : 'center',
'time' : 0.1,
'custom': {'x': 0, 'y': 0, 'dz': 0},
'coord' : {'x': 0, 'y': 0, 'dz': 0}}
variable_extruder: {'fw_retract': False,
'retract': 1.0,
'extrude': 1.0}
variable_speed: {'travel': 100,
'retract': 15,
'extrude': 15}
variable_verbose: True
variable_check_time: 0.5
variable_restore: {'absolute': {'coordinates': True, 'extrude': True}, 'speed': 1500, 'e':0, 'factor': {'speed': 1.0, 'extrude': 1.0}}
variable_macro: {'pause': 'PAUSE', 'resume': 'RESUME'}
variable_is_paused: False
gcode:
{% set hyperlapse = True if params.HYPERLAPSE and params.HYPERLAPSE|lower =='true' else False %}
{% if enable %}
{% if (hyperlapse and printer['gcode_macro HYPERLAPSE'].run) or
(not hyperlapse and not printer['gcode_macro HYPERLAPSE'].run) %}
{% if park.enable %}
{% set pos = {'x': 'X' + park.coord.x|string if park.pos != 'y_only' else '',
'y': 'Y' + park.coord.y|string if park.pos != 'x_only' else '',
'z': 'Z'+ [printer.gcode_move.gcode_position.z + park.coord.dz, printer.toolhead.axis_maximum.z]|min|string} %}
{% set restore = {'absolute': {'coordinates': printer.gcode_move.absolute_coordinates,
'extrude' : printer.gcode_move.absolute_extrude},
'speed' : printer.gcode_move.speed,
'e' : printer.gcode_move.gcode_position.e,
'factor' : {'speed' : printer.gcode_move.speed_factor,
'extrude': printer.gcode_move.extrude_factor}} %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=restore VALUE="{restore}"
{% if not printer[printer.toolhead.extruder].can_extrude %}
{% if verbose %}{action_respond_info("Timelapse: Warning, minimum extruder temperature not reached!")}{% endif %}
{% else %}
{% if extruder.fw_retract %}
G10
{% else %}
M83 ; insure relative extrusion
G0 E-{extruder.retract} F{speed.retract * 60}
{% endif %}
{% endif %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=is_paused VALUE=True
{macro.pause} ; execute the klipper PAUSE command
SET_GCODE_OFFSET X=0 Y=0 ; this will insure that the head parks always at the same position in a multi setup
G90 ; insure absolute move
{% if "xyz" not in printer.toolhead.homed_axes %}
{% if verbose %}{action_respond_info("Timelapse: Warning, axis not homed yet!")}{% endif %}
{% else %}
G0 {pos.x} {pos.y} {pos.z} F{speed.travel * 60}
{% endif %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=takingframe VALUE=True
UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_TAKE_FRAME DURATION={check_time}
M400
{% endif %}
_TIMELAPSE_NEW_FRAME HYPERLAPSE={hyperlapse}
{% endif %}
{% else %}
{% if verbose %}{action_respond_info("Timelapse: disabled, take frame ignored")}{% endif %}
{% endif %}
[gcode_macro _TIMELAPSE_NEW_FRAME]
description: action call for timelapse shoot. must be a seperate macro
gcode:
{action_call_remote_method("timelapse_newframe",
macropark=printer['gcode_macro TIMELAPSE_TAKE_FRAME'].park,
hyperlapse=params.HYPERLAPSE)}
[delayed_gcode _WAIT_TIMELAPSE_TAKE_FRAME]
gcode:
{% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %}
{% set factor = {'speed': printer.gcode_move.speed_factor, 'extrude': printer.gcode_move.extrude_factor} %}
{% if tl.takingframe %}
UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_TAKE_FRAME DURATION={tl.check_time}
{% else %}
{tl.macro.resume} VELOCITY={tl.speed.travel} ; execute the klipper RESUME command
SET_GCODE_VARIABLE MACRO=TIMELAPSE_TAKE_FRAME VARIABLE=is_paused VALUE=False
{% if not printer[printer.toolhead.extruder].can_extrude %}
{action_respond_info("Timelapse: Warning minimum extruder temperature not reached!")}
{% else %}
{% if tl.extruder.fw_retract %}
G11
{% else %}
G0 E{tl.extruder.extrude} F{tl.speed.extrude * 60}
G0 F{tl.restore.speed}
{% if tl.restore.absolute.extrude %}
M82
G92 E{tl.restore.e}
{% endif %}
{% endif %}
{% endif %}
{% if tl.restore.factor.speed != factor.speed %} M220 S{(factor.speed*100)|round(0)} {% endif %}
{% if tl.restore.factor.extrude != factor.extrude %} M221 S{(factor.extrude*100)|round(0)} {% endif %}
{% if not tl.restore.absolute.coordinates %} G91 {% endif %}
{% endif %}
####################################################################################################
# #
# HYPERLAPSE: Starts or stops a Hyperlapse video #
# Usage: HYPERLAPSE ACTION=START [CYCLE=time] starts a hyperlapse with cycle time (default 30 sec) #
# HYPERLAPSE ACTION=STOP stops the hyperlapse recording #
# #
####################################################################################################
######################### definition #########################
## cycle: cycle time in seconds
## run: internal use [True/False]
###############################################################
[gcode_macro HYPERLAPSE]
description: Start/Stop a hyperlapse recording
variable_cycle: 0
variable_run: False
gcode:
{% set cycle = params.CYCLE|default(30)|int %}
{% if params.ACTION and params.ACTION|lower == 'start' %}
{action_respond_info("Hyperlapse: frames started (Cycle %d sec)" % cycle)}
SET_GCODE_VARIABLE MACRO=HYPERLAPSE VARIABLE=run VALUE=True
SET_GCODE_VARIABLE MACRO=HYPERLAPSE VARIABLE=cycle VALUE={cycle}
UPDATE_DELAYED_GCODE ID=_HYPERLAPSE_LOOP DURATION={cycle}
TIMELAPSE_TAKE_FRAME HYPERLAPSE=True
{% elif params.ACTION and params.ACTION|lower == 'stop' %}
{% if run %}{action_respond_info("Hyperlapse: frames stopped")}{% endif %}
SET_GCODE_VARIABLE MACRO=HYPERLAPSE VARIABLE=run VALUE=False
UPDATE_DELAYED_GCODE ID=_HYPERLAPSE_LOOP DURATION=0
{% else %}
{action_raise_error("Hyperlapse: No valid input parameter
Use:
- HYPERLAPSE ACTION=START [CYCLE=time]
- HYPERLAPSE ACTION=STOP")}
{% endif %}
[delayed_gcode _HYPERLAPSE_LOOP]
gcode:
UPDATE_DELAYED_GCODE ID=_HYPERLAPSE_LOOP DURATION={printer["gcode_macro HYPERLAPSE"].cycle}
TIMELAPSE_TAKE_FRAME HYPERLAPSE=True
##########################################################################
# #
# TIMELAPSE_RENDER: Render the video at print end #
# #
##########################################################################
######################### definition #########################
## render: internal use. Valid inputs: [True, False]
## run_identifier: internal use. Valid input [0 .. 3]
###############################################################
[gcode_macro TIMELAPSE_RENDER]
description: Render Timelapse video and wait for the result
variable_render: False
variable_run_identifier: 0
gcode:
{action_respond_info("Timelapse: Rendering started")}
{action_call_remote_method("timelapse_render", byrendermacro="True")}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_RENDER VARIABLE=render VALUE=True
{printer.configfile.settings['gcode_macro pause'].rename_existing} ; execute the klipper PAUSE command
UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_RENDER DURATION=0.5
[delayed_gcode _WAIT_TIMELAPSE_RENDER]
gcode:
{% set ri = printer['gcode_macro TIMELAPSE_RENDER'].run_identifier % 4 %}
SET_GCODE_VARIABLE MACRO=TIMELAPSE_RENDER VARIABLE=run_identifier VALUE={ri + 1}
{% if printer['gcode_macro TIMELAPSE_RENDER'].render %}
M117 Rendering {['-','\\','|','/'][ri]}
UPDATE_DELAYED_GCODE ID=_WAIT_TIMELAPSE_RENDER DURATION=0.5
{% else %}
{action_respond_info("Timelapse: Rendering finished")}
M117
{printer.configfile.settings['gcode_macro resume'].rename_existing} ; execute the klipper RESUME command
{% endif %}
##########################################################################
# #
# TEST_STREAM_DELAY: Helper macro to find stream and park delay #
# #
##########################################################################
[gcode_macro TEST_STREAM_DELAY]
description: Helper macro to find stream and park delay
gcode:
{% set min = printer.toolhead.axis_minimum %}
{% set max = printer.toolhead.axis_maximum %}
{% set act = printer.toolhead.position %}
{% set tl = printer['gcode_macro TIMELAPSE_TAKE_FRAME'] %}
{% if act.z > 5.0 %}
G0 X{min.x + 5.0} F{tl.speed.travel|int * 60}
G0 X{(max.x-min.x)/2}
G4 P{tl.park.time|float * 1000}
_TIMELAPSE_NEW_FRAME HYPERLAPSE=FALSE
G0 X{max.x - 5.0}
{% else %}
{action_raise_error("Toolhead z %.3f to low. Please place head above z = 5.0" % act.z)}
{% endif %}

View file

@ -0,0 +1,842 @@
# Moonraker Timelapse component for K1 Series
#
# Copyright (C) 2021 Christoph Frei <fryakatkop@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
from __future__ import annotations
import logging
import os
import glob
import re
import shutil
import asyncio
from datetime import datetime
from tornado.ioloop import IOLoop
from zipfile import ZipFile
# Annotation imports
from typing import (
TYPE_CHECKING,
Dict,
Any
)
if TYPE_CHECKING:
from confighelper import ConfigHelper
from .webcam import WebcamManager, WebCam
from websockets import WebRequest
from . import shell_command
from . import klippy_apis
from . import database
APIComp = klippy_apis.KlippyAPI
SCMDComp = shell_command.ShellCommandFactory
DBComp = database.MoonrakerDatabase
class Timelapse:
def __init__(self, confighelper: ConfigHelper) -> None:
# setup vars
self.renderisrunning = False
self.saveisrunning = False
self.takingframe = False
self.framecount = 0
self.lastframefile = ""
self.lastrenderprogress = 0
self.lastcmdreponse = ""
self.byrendermacro = False
self.hyperlapserunning = False
self.printing = False
self.noWebcamDb = False
self.confighelper = confighelper
self.server = confighelper.get_server()
self.klippy_apis: APIComp = self.server.lookup_component('klippy_apis')
self.database: DBComp = self.server.lookup_component("database")
# setup static (nonDB) settings
out_dir_cfg = confighelper.get(
"output_path", "~/timelapse/")
temp_dir_cfg = confighelper.get(
"frame_path", "/tmp/timelapse/")
self.ffmpeg_binary_path = confighelper.get(
"ffmpeg_binary_path", "/opt/bin/ffmpeg")
self.wget_skip_cert = confighelper.getboolean(
"wget_skip_cert_check", False)
# Setup default config
self.config: Dict[str, Any] = {
'enabled': True,
'mode': "layermacro",
'camera': "",
'snapshoturl': "http://localhost:8080/?action=snapshot",
'stream_delay_compensation': 0.05,
'gcode_verbose': False,
'parkhead': False,
'parkpos': "back_right",
'park_custom_pos_x': 10.0,
'park_custom_pos_y': 10.0,
'park_custom_pos_dz': 0.0,
'park_travel_speed': 400,
'park_retract_speed': 40,
'park_extrude_speed': 40,
'park_retract_distance': 0.5,
'park_extrude_distance': 0.5,
'park_time': 0.1,
'fw_retract': False,
'hyperlapse_cycle': 30,
'autorender': True,
'constant_rate_factor': 23,
'output_framerate': 30,
'pixelformat': "yuv420p",
'time_format_code': "%d-%m-%Y_%Hh%M",
'extraoutputparams': "",
'variable_fps': False,
'targetlength': 10,
'variable_fps_min': 5,
'variable_fps_max': 60,
'rotation': 0,
'flip_x': False,
'flip_y': False,
'duplicatelastframe': 5,
'previewimage': True,
'saveframes': False
}
# Get Config from Database and overwrite defaults
dbconfig: Dict[str, Any] = self.database.get_item("timelapse",
"config",
self.config)
if isinstance(dbconfig, asyncio.Future):
self.config.update(dbconfig.result())
else:
self.config.update(dbconfig)
# Overwrite Config with fixed config made in moonraker.conf
# this is a fallback to older setups and when the Frontend doesn't
# support the settings endpoint
self.overwriteDbconfigWithConfighelper()
# check if ffmpeg is installed
self.ffmpeg_installed = os.path.isfile(self.ffmpeg_binary_path)
if not self.ffmpeg_installed:
self.config['autorender'] = False
logging.info(f"timelapse: {self.ffmpeg_binary_path} \
not found please install to use render functionality")
# setup directories
# remove trailing "/"
out_dir_cfg = os.path.join(out_dir_cfg, '')
temp_dir_cfg = os.path.join(temp_dir_cfg, '')
# evaluate and expand "~"
self.out_dir = os.path.expanduser(out_dir_cfg)
self.temp_dir = os.path.expanduser(temp_dir_cfg)
# create directories if they doesn't exist
os.makedirs(self.temp_dir, exist_ok=True)
os.makedirs(self.out_dir, exist_ok=True)
# setup eventhandlers and endpoints
file_manager = self.server.lookup_component("file_manager")
file_manager.register_directory("timelapse",
self.out_dir,
full_access=True
)
file_manager.register_directory("timelapse_frames", self.temp_dir)
self.server.register_notification("timelapse:timelapse_event")
self.server.register_event_handler(
"server:gcode_response", self.handle_gcode_response)
self.server.register_event_handler(
"server:status_update", self.handle_status_update)
self.server.register_event_handler(
"server:klippy_ready", self.handle_klippy_ready)
self.server.register_remote_method(
"timelapse_newframe", self.call_newframe)
self.server.register_remote_method(
"timelapse_saveFrames", self.call_saveFramesZip)
self.server.register_remote_method(
"timelapse_render", self.call_render)
self.server.register_endpoint(
"/machine/timelapse/render", ['POST'], self.render)
self.server.register_endpoint(
"/machine/timelapse/saveframes", ['POST'], self.saveFramesZip)
self.server.register_endpoint(
"/machine/timelapse/settings", ['GET', 'POST'],
self.webrequest_settings)
self.server.register_endpoint(
"/machine/timelapse/lastframeinfo", ['GET'],
self.webrequest_lastframeinfo)
async def component_init(self) -> None:
await self.getWebcamConfig()
def overwriteDbconfigWithConfighelper(self) -> None:
blockedsettings = []
for config in self.confighelper.get_options():
if config in self.config:
configtype = type(self.config[config])
if configtype == str:
self.config[config] = self.confighelper.get(config)
elif configtype == bool:
self.config[config] = self.confighelper.getboolean(config)
elif configtype == int:
self.config[config] = self.confighelper.getint(config)
elif configtype == float:
self.config[config] = self.confighelper.getfloat(config)
# add the config to list of blockedsettings
blockedsettings.append(config)
# append the list of blockedsettings to the config dict
self.config.update({'blockedsettings': blockedsettings})
logging.debug(f"blockedsettings {self.config['blockedsettings']}")
async def getWebcamConfig(self) -> None:
# Read Webcam config from Database
webcam_name = self.config['camera']
try:
wcmgr: WebcamManager = self.server.lookup_component("webcam")
cams = wcmgr.get_webcams()
if not cams:
logging.info("WARNING: no camera configured, " +
"using the fallback config")
fallback = {'snapshot_url': self.config['snapshoturl'],
'rotation': self.config['rotation'],
'flip_horizontal': self.config['flip_x'],
'flip_vertical': self.config['flip_y']
}
self.parseWebcamConfig(fallback)
return
if webcam_name and webcam_name in cams:
camera = cams[webcam_name]
else:
camera = list(cams.values())[0]
self.parseWebcamConfig(camera.as_dict())
except Exception as e:
logging.info(f"something went wrong getting"
f"Cam Camera:{webcam_name} from Database. "
f"Exception: {e}"
)
def parseWebcamConfig(self, webcamconfig) -> None:
snapshoturl = webcamconfig['snapshot_url']
flip_x = webcamconfig['flip_horizontal']
flip_y = webcamconfig['flip_vertical']
rotation = webcamconfig['rotation']
oldWebcamConfig = {"url": self.config['snapshoturl'],
"flip_x": self.config['flip_x'],
"flip_y": self.config['flip_y'],
"rotation": self.config['rotation']
}
self.config['snapshoturl'] = self.confighelper.get('snapshoturl',
snapshoturl
)
self.config['flip_x'] = self.confighelper.getboolean('flip_x',
flip_x
)
self.config['flip_y'] = self.confighelper.getboolean('flip_y',
flip_y
)
self.config['rotation'] = self.confighelper.getint('rotation',
rotation
)
if not self.config['snapshoturl'].startswith('http'):
if not self.config['snapshoturl'].startswith('/'):
self.config['snapshoturl'] = "http://localhost/" + \
self.config['snapshoturl']
else:
self.config['snapshoturl'] = "http://localhost" + \
self.config['snapshoturl']
# check if settings have changed and if so creat log entry
newWebcamConfig = {"url": self.config['snapshoturl'],
"flip_x": self.config['flip_x'],
"flip_y": self.config['flip_y'],
"rotation": self.config['rotation']
}
if not oldWebcamConfig == newWebcamConfig:
logging.info("snapshoturl: "
f"{self.config['snapshoturl']}, "
f"Flip V/H: {self.config['flip_y']}/"
f"{self.config['flip_y']}, "
f"rotation: {self.config['rotation']}"
)
async def webrequest_lastframeinfo(self,
webrequest: WebRequest
) -> Dict[str, Any]:
return {
'framecount': self.framecount,
'lastframefile': self.lastframefile
}
async def webrequest_settings(self,
webrequest: WebRequest
) -> Dict[str, Any]:
action = webrequest.get_action()
if action == 'POST':
args = webrequest.get_args()
logging.debug("webreq_args: " + str(args))
gcodechange = False
settingsWithGcodechange = [
'enabled', 'parkhead',
'parkpos', 'park_custom_pos_x',
'park_custom_pos_y', 'park_custom_pos_dz',
'park_travel_speed', 'park_retract_speed',
'park_extrude_speed', 'park_retract_distance',
'park_extrude_distance', 'park_time', 'fw_retract'
]
modechanged = False
for setting in args:
if setting in self.config:
settingtype = type(self.config[setting])
if setting == "snapshoturl":
logging.debug(
"snapshoturl cannot be changed via webrequest")
elif settingtype == str:
settingvalue = webrequest.get(setting)
elif settingtype == bool:
settingvalue = webrequest.get_boolean(setting)
elif settingtype == int:
settingvalue = webrequest.get_int(setting)
elif settingtype == float:
settingvalue = webrequest.get_float(setting)
self.config[setting] = settingvalue
self.database.insert_item(
"timelapse",
f"config.{setting}",
settingvalue
)
if setting == "camera":
if not self.noWebcamDb:
await self.getWebcamConfig()
else:
logging.info("Webcam Namespace not intialized, "
"please restart moonraker service!")
if setting in settingsWithGcodechange:
gcodechange = True
if setting == "mode":
modechanged = True
logging.debug(f"changed setting: {setting} "
f"value: {settingvalue} "
f"type: {settingtype}"
)
if modechanged:
if self.config['mode'] == "hyperlapse":
if not self.hyperlapserunning:
if self.printing:
ioloop = IOLoop.current()
ioloop.spawn_callback(self.start_hyperlapse)
else:
if self.hyperlapserunning:
ioloop = IOLoop.current()
ioloop.spawn_callback(self.stop_hyperlapse)
if gcodechange:
ioloop = IOLoop.current()
ioloop.spawn_callback(self.setgcodevariables)
return self.config
async def handle_klippy_ready(self) -> None:
ioloop = IOLoop.current()
ioloop.spawn_callback(self.setgcodevariables)
ioloop = IOLoop.current()
ioloop.spawn_callback(self.stop_hyperlapse)
async def setgcodevariables(self) -> None:
gcommand = "_SET_TIMELAPSE_SETUP " \
+ f" ENABLE={self.config['enabled']}" \
+ f" VERBOSE={self.config['gcode_verbose']}" \
+ f" PARK_ENABLE={self.config['parkhead']}" \
+ f" PARK_POS={self.config['parkpos']}" \
+ f" CUSTOM_POS_X={self.config['park_custom_pos_x']}" \
+ f" CUSTOM_POS_Y={self.config['park_custom_pos_y']}" \
+ f" CUSTOM_POS_DZ={self.config['park_custom_pos_dz']}" \
+ f" TRAVEL_SPEED={self.config['park_travel_speed']}" \
+ f" RETRACT_SPEED={self.config['park_retract_speed']}" \
+ f" EXTRUDE_SPEED={self.config['park_extrude_speed']}" \
+ f" RETRACT_DISTANCE={self.config['park_retract_distance']}" \
+ f" EXTRUDE_DISTANCE={self.config['park_extrude_distance']}" \
+ f" PARK_TIME={self.config['park_time']}" \
+ f" FW_RETRACT={self.config['fw_retract']}" \
logging.debug(f"run gcommand: {gcommand}")
try:
await self.klippy_apis.run_gcode(gcommand)
except self.server.error:
msg = f"Error executing GCode {gcommand}"
logging.exception(msg)
def call_newframe(self, macropark=False, hyperlapse=False) -> None:
if self.config['enabled']:
if self.config['mode'] == "hyperlapse":
if hyperlapse:
if not self.takingframe:
self.takingframe = True
self.spawn_newframe_callbacks()
else:
logging.info("last take frame hasn't completed"
+ " ignoring take frame command"
)
else:
logging.info("ignoring non hyperlapse triggered macros"
+ "in hyperlapse mode"
)
else:
self.spawn_newframe_callbacks()
else:
logging.info("NEW_FRAME macro ignored timelapse is disabled")
def spawn_newframe_callbacks(self) -> None:
ioloop = IOLoop.current()
# release parked head after park time is passed
park_time = self.config['park_time']
ioloop.call_later(delay=park_time, callback=self.release_parkedhead)
# capture the frame after stream delay is passed
stream_delay = self.config['stream_delay_compensation']
ioloop.call_later(delay=stream_delay, callback=self.newframe)
async def release_parkedhead(self) -> None:
gcommand = "SET_GCODE_VARIABLE " \
+ "MACRO=TIMELAPSE_TAKE_FRAME " \
+ "VARIABLE=takingframe VALUE=False"
logging.debug(f"run gcommand: {gcommand}")
try:
await self.klippy_apis.run_gcode(gcommand)
except self.server.error:
msg = f"Error executing GCode {gcommand}"
logging.exception(msg)
async def start_hyperlapse(self) -> None:
hyperlapse_cycle = self.config['hyperlapse_cycle']
park_time = self.config['park_time']
timediff = hyperlapse_cycle - park_time
if timediff >= 1:
gcommand = "HYPERLAPSE ACTION=START" \
+ f" CYCLE={hyperlapse_cycle}"
logging.debug(f"run gcommand: {gcommand}")
try:
await self.klippy_apis.run_gcode(gcommand)
except self.server.error:
msg = f"Error executing GCode {gcommand}"
logging.exception(msg)
self.hyperlapserunning = True
else:
logging.info("WARNING: Blocked start of Hyperlapse, because "
f"hyperlapse_cycle ({hyperlapse_cycle}s) is smaller "
f"then or to close to park_time ({park_time}s)"
)
async def stop_hyperlapse(self) -> None:
gcommand = "HYPERLAPSE ACTION=STOP"
logging.debug(f"run gcommand: {gcommand}")
try:
await self.klippy_apis.run_gcode(gcommand)
except self.server.error:
msg = f"Error executing GCode {gcommand}"
logging.exception(msg)
self.hyperlapserunning = False
async def newframe(self) -> None:
# make sure webcamconfig is uptodate before grabbing a new frame
await self.getWebcamConfig()
options = ""
if self.wget_skip_cert:
options += "--no-check-certificate "
self.framecount += 1
framefile = "frame" + str(self.framecount).zfill(6) + ".jpg"
cmd = "wget " + options + self.config['snapshoturl'] \
+ " -O " + self.temp_dir + framefile
self.lastframefile = framefile
logging.debug(f"cmd: {cmd}")
shell_cmd: SCMDComp = self.server.lookup_component('shell_command')
scmd = shell_cmd.build_shell_command(cmd, None)
try:
cmdstatus = await scmd.run(timeout=2., verbose=False)
except Exception:
logging.exception(f"Error running cmd '{cmd}'")
result = {'action': 'newframe'}
if cmdstatus:
result.update({
'frame': str(self.framecount),
'framefile': framefile,
'status': 'success'
})
else:
logging.info(f"getting newframe failed: {cmd}")
self.framecount -= 1
result.update({'status': 'error'})
self.notify_event(result)
self.takingframe = False
async def handle_status_update(self, status: Dict[str, Any]) -> None:
if 'print_stats' in status:
printstats = status['print_stats']
if 'state' in printstats:
state = printstats['state']
if state == 'cancelled':
self.printing = False
ioloop = IOLoop.current()
ioloop.spawn_callback(self.stop_hyperlapse)
async def handle_gcode_response(self, gresponse: str) -> None:
if gresponse == "File selected":
# print_started
self.cleanup()
self.printing = True
# start hyperlapse if mode is set
if self.config['mode'] == "hyperlapse":
ioloop = IOLoop.current()
ioloop.spawn_callback(self.start_hyperlapse)
elif gresponse == "Done printing file":
# print_done
self.printing = False
# stop hyperlapse if mode is set
if self.config['mode'] == "hyperlapse":
ioloop = IOLoop.current()
ioloop.spawn_callback(self.stop_hyperlapse)
if self.config['enabled']:
if self.config['saveframes']:
ioloop = IOLoop.current()
ioloop.spawn_callback(self.saveFramesZip)
if self.config['autorender']:
ioloop = IOLoop.current()
ioloop.spawn_callback(self.render)
def cleanup(self) -> None:
logging.debug("cleanup frame directory")
filelist = glob.glob(self.temp_dir + "frame*.jpg")
if filelist:
for filepath in filelist:
os.remove(filepath)
self.framecount = 0
self.lastframefile = ""
def call_saveFramesZip(self) -> None:
ioloop = IOLoop.current()
ioloop.spawn_callback(self.saveFramesZip)
async def saveFramesZip(self, webrequest=None):
filelist = sorted(glob.glob(self.temp_dir + "frame*.jpg"))
self.framecount = len(filelist)
result = {'action': 'saveframes'}
if not filelist:
msg = "no frames to save, skip"
status = "skipped"
elif self.saveisrunning:
msg = "saving frames already"
status = "running"
else:
self.saveisrunning = True
# get printed filename
kresult = await self.klippy_apis.query_objects(
{'print_stats': None})
pstats = kresult.get("print_stats", {})
gcodefilename = pstats.get("filename", "").split("/")[-1]
# prepare output filename
now = datetime.now()
date_time = now.strftime(self.config['time_format_code'])
outfile = f"k1_{gcodefilename}_{date_time}"
outfileFull = outfile + "_frames.zip"
zipObj = ZipFile(self.out_dir + outfileFull, "w")
for frame in filelist:
zipObj.write(frame, frame.split("/")[-1])
logging.info(f"saved frames: {outfile}_frames.zip")
result.update({
'status': 'finished',
'zipfile': outfileFull
})
self.saveisrunning = False
return result
def call_render(self, byrendermacro=False) -> None:
self.byrendermacro = byrendermacro
ioloop = IOLoop.current()
ioloop.spawn_callback(self.render)
async def render(self, webrequest=None):
filelist = sorted(glob.glob(self.temp_dir + "frame*.jpg"))
self.framecount = len(filelist)
result = {'action': 'render'}
# make sure webcamconfig is uptodate for the rotation/flip feature
await self.getWebcamConfig()
if not filelist:
msg = "no frames to render, skip"
status = "skipped"
elif self.renderisrunning:
msg = "render is already running"
status = "running"
elif not self.ffmpeg_installed:
msg = f"{self.ffmpeg_binary_path} not found, please install ffmpeg"
status = "error"
# cmd = outfile = None
logging.info(f"timelapse: {msg}")
else:
self.renderisrunning = True
# get printed filename
kresult = await self.klippy_apis.query_objects(
{'print_stats': None})
pstats = kresult.get("print_stats", {})
gcodefilename = pstats.get("filename", "").split("/")[-1]
# prepare output filename
now = datetime.now()
date_time = now.strftime(self.config['time_format_code'])
inputfiles = self.temp_dir + "frame%6d.jpg"
outfile = f"k1_{gcodefilename}_{date_time}"
# dublicate last frame
duplicates = []
if self.config['duplicatelastframe'] > 0:
lastframe = filelist[-1:][0]
for i in range(self.config['duplicatelastframe']):
nextframe = str(self.framecount + i + 1).zfill(6)
duplicate = "frame" + nextframe + ".jpg"
duplicatePath = self.temp_dir + duplicate
duplicates.append(duplicatePath)
try:
shutil.copy(lastframe, duplicatePath)
except OSError as err:
logging.info(f"duplicating last frame failed: {err}")
# update Filelist
filelist = sorted(glob.glob(self.temp_dir + "frame*.jpg"))
self.framecount = len(filelist)
# variable framerate
if self.config['variable_fps']:
fps = int(self.framecount / self.config['targetlength'])
fps = max(min(fps,
self.config['variable_fps_max']),
self.config['variable_fps_min'])
else:
fps = self.config['output_framerate']
# apply rotation
filterParam = ""
if self.config['rotation'] == 90 and self.config['flip_y']:
filterParam = " -vf 'transpose=3'"
elif self.config['rotation'] == 90:
filterParam = " -vf 'transpose=1'"
elif self.config['rotation'] == 180:
filterParam = " -vf 'hflip,vflip'"
elif self.config['rotation'] == 270:
filterParam = " -vf 'transpose=2'"
elif self.config['rotation'] == 270 and self.config['flip_y']:
filterParam = " -vf 'transpose=0'"
elif self.config['rotation'] > 0:
pi = 3.141592653589793
rot = str(self.config['rotation']*(pi/180))
filterParam = " -vf 'rotate=" + rot + "'"
elif self.config['flip_x'] and self.config['flip_y']:
filterParam = " -vf 'hflip,vflip'"
elif self.config['flip_x']:
filterParam = " -vf 'hflip'"
elif self.config['flip_y']:
filterParam = " -vf 'vflip'"
# build shell command
cmd = self.ffmpeg_binary_path \
+ " -r " + str(fps) \
+ " -i '" + inputfiles + "'" \
+ filterParam \
+ " -threads 2 -g 5" \
+ " -crf " + str(self.config['constant_rate_factor']) \
+ " -vcodec libx264" \
+ " -pix_fmt " + self.config['pixelformat'] \
+ " -preset superfast" \
+ " -an" \
+ " " + self.config['extraoutputparams'] \
+ " '" + self.temp_dir + outfile + ".mp4' -y"
# log and notify ws
logging.info(f"start FFMPEG: {cmd}")
result.update({
'status': 'started',
'framecount': str(self.framecount),
'settings': {
'framerate': fps,
'crf': self.config['constant_rate_factor'],
'pixelformat': self.config['pixelformat']
}
})
# run the command
shell_cmd: SCMDComp = self.server.lookup_component('shell_command')
self.notify_event(result)
scmd = shell_cmd.build_shell_command(cmd, self.ffmpeg_cb)
try:
cmdstatus = await scmd.run(verbose=True,
log_complete=False,
timeout=9999999999,
)
except Exception:
logging.exception(f"Error running cmd '{cmd}'")
# check success
if cmdstatus:
status = "success"
msg = f"Rendering Video successful: {outfile}.mp4"
result.update({
'filename': f"{outfile}.mp4",
'printfile': gcodefilename
})
# result.pop("framecount")
result.pop("settings")
# move finished output file to output directory
try:
shutil.move(self.temp_dir + outfile + ".mp4",
self.out_dir + outfile + ".mp4")
except OSError as err:
logging.info(f"moving output file failed: {err}")
# copy image preview
if self.config['previewimage']:
previewFile = f"{outfile}.jpg"
previewFilePath = self.out_dir + previewFile
previewSrc = filelist[-1:][0]
try:
shutil.copy(previewSrc, previewFilePath)
except OSError as err:
logging.info(f"copying preview image failed: {err}")
else:
result.update({
'previewimage': previewFile
})
# apply rotation previewimage if needed
if filterParam or self.config['extraoutputparams']:
cmd = self.ffmpeg_binary_path \
+ " -i '" + previewFilePath + "'" \
+ filterParam \
+ " -an" \
+ " " + self.config['extraoutputparams'] \
+ " '" + previewFilePath + "' -y"
logging.info(f"Rotate preview image cmd: {cmd}")
scmd = shell_cmd.build_shell_command(cmd)
try:
cmdstatus = await scmd.run(verbose=True,
log_complete=False,
timeout=9999999999,
)
except Exception:
logging.exception(f"Error running cmd '{cmd}'")
else:
status = "error"
msg = f"Rendering Video failed: {cmd} : {self.lastcmdreponse}"
result.update({
'cmd': cmd,
'cmdresponse': self.lastcmdreponse
})
self.renderisrunning = False
# cleanup duplicates
if duplicates:
for dupe in duplicates:
try:
os.remove(dupe)
except OSError as err:
logging.info(f"remove duplicate failed: {err}")
# log and notify ws
logging.info(msg)
result.update({
'status': status,
'msg': msg
})
self.notify_event(result)
# confirm render finish to stop the render macro loop
if self.byrendermacro:
gcommand = "SET_GCODE_VARIABLE " \
+ "MACRO=TIMELAPSE_RENDER VARIABLE=render VALUE=False"
logging.debug(f"run gcommand: {gcommand}")
try:
await self.klippy_apis.run_gcode(gcommand)
except self.server.error:
msg = f"Error executing GCode {gcommand}"
logging.exception(msg)
self.byrendermacro = False
return result
def ffmpeg_cb(self, response):
# logging.debug(f"ffmpeg_cb: {response}")
self.lastcmdreponse = response.decode("utf-8")
try:
frame = re.search(
r'(?<=frame=)*(\d+)(?=.+fps)', self.lastcmdreponse
).group()
except AttributeError:
return
percent = int(frame) / self.framecount * 100
if percent > 100:
percent = 100
if self.lastrenderprogress != int(percent):
self.lastrenderprogress = int(percent)
# logging.debug(f"ffmpeg Progress: {self.lastrenderprogress}% ")
result = {
'action': 'render',
'status': 'running',
'progress': self.lastrenderprogress
}
self.notify_event(result)
def notify_event(self, result: Dict[str, Any]) -> None:
logging.debug(f"notify_event: {result}")
self.server.send_event("timelapse:timelapse_event", result)
def load_component(config: ConfigHelper) -> Timelapse:
return Timelapse(config)

View file

@ -0,0 +1,13 @@
klipper_mcu
webcamd
MoonCord
KlipperScreen
moonraker-telegram-bot
moonraker-obico
sonar
crowsnest
octoeverywhere
ratos-configurator
mobileraker
guppyscreen
Git-Backup

View file

@ -0,0 +1,103 @@
[server]
host: 0.0.0.0
port: 7125
klippy_uds_address: /tmp/klippy_uds
max_upload_size: 1024
[file_manager]
queue_gcode_uploads: False
enable_object_processing: True
[database]
[data_store]
temperature_store_size: 600
gcode_store_size: 1000
[machine]
provider: supervisord_cli
validate_service: False
validate_config: False
[authorization]
force_logins: False
cors_domains:
*.lan
*.local
*://localhost
*://localhost:*
*://my.mainsail.xyz
*://app.fluidd.xyz
trusted_clients:
10.0.0.0/8
127.0.0.0/8
169.254.0.0/16
172.16.0.0/12
192.168.0.0/16
FE80::/10
::1/128
[octoprint_compat]
[history]
[update_manager]
enable_auto_refresh: True
refresh_interval: 24
enable_system_updates: False
# Remove '#' after this line to keep Creality Helper Script up to date
[update_manager Creality-Helper-Script]
type: git_repo
channel: dev
path: /usr/data/helper-script
origin: https://github.com/Guilouz/Creality-Helper-Script.git
primary_branch: master
managed_services: klipper
# Remove '#' after this line to enable camera configuration with Moonraker and replace 'xxx.xxx.xxx.xxx' by your IP addresses
#[webcam Camera]
#location: printer
#enabled: True
#service: mjpegstreamer
#target_fps: 15
#target_fps_idle: 5
#stream_url: http://xxx.xxx.xxx.xxx:8080/?action=stream
#snapshot_url: http://xxx.xxx.xxx.xxx:8080/?action=snapshot
#flip_horizontal: False
#flip_vertical: False
#rotation: 0
#aspect_ratio: 4:3
# Remove '#' after this line if you use Timelapse function and replace port '4408' by '4409' in snapshoturl if you use Mainsail
#[timelapse]
#output_path: /usr/data/printer_data/timelapse/
#frame_path: /usr/data/printer_data/frames/
#ffmpeg_binary_path: /opt/bin/ffmpeg
#snapshoturl: http://localhost:8080/?action=snapshot
# Remove '#' after this line if you use Fluidd
#[update_manager fluidd]
#type: web
#channel: beta
#repo: fluidd-core/fluidd
#path: /usr/data/fluidd
# Remove '#' after this line if you use Mainsail
#[update_manager mainsail]
#type: web
#channel: beta
#repo: mainsail-crew/mainsail
#path: /usr/data/mainsail
# Remove '#' after this line if you use Mobileraker Companion
#[update_manager mobileraker]
#type: git_repo
#path: /usr/data/mobileraker_companion
#origin: https://github.com/Clon1998/mobileraker_companion.git
#virtualenv: /usr/data/mobileraker-env
#primary_branch:main
#requirements: scripts/mobileraker-requirements.txt
#install_script: scripts/install.sh
#managed_services: mobileraker

Binary file not shown.

View file

@ -0,0 +1,5 @@
from .prtouch_v2_fan import PRTouchFan
def load_config(config):
return PRTouchFan(config)

View file

@ -0,0 +1,6 @@
########################################
# Nozzle Cleaning Fan Control
########################################
[prtouch_v2_fan]
max_speed: 0.5

Binary file not shown.

View file

@ -0,0 +1,25 @@
########################################
# Screws Tilt Adjust for K1
########################################
[screws_tilt_adjust]
screw1: 25,20
screw1_name: front left screw
screw2: 195,20
screw2_name: front right screw
screw3: 195,190
screw3_name: rear right screw
screw4: 25,190
screw4_name: rear left screw
speed: 100
horizontal_move_z: 5
screw_thread: CW-M4
[gcode_macro SCREWS_CALIBRATION]
description: Start Bed Screws Calibration
gcode:
{% if printer.toolhead.homed_axes != "xyz" %}
G28
{% endif %}
SCREWS_TILT_CALCULATE

View file

@ -0,0 +1,25 @@
########################################
# Screws Tilt Adjust for K1 Max
########################################
[screws_tilt_adjust]
screw1: 19,23
screw1_name: front left screw
screw2: 278,23
screw2_name: front right screw
screw3: 248,272
screw3_name: rear right screw
screw4: 48,272
screw4_name: rear left screw
horizontal_move_z: 5
speed: 150
screw_thread: CW-M4
[gcode_macro SCREWS_CALIBRATION]
description: Start Bed Screws Calibration
gcode:
{% if printer.toolhead.homed_axes != "xyz" %}
G28
{% endif %}
SCREWS_TILT_CALCULATE

View file

@ -0,0 +1,131 @@
# Helper script to adjust bed screws tilt using Z probe
#
# Copyright (C) 2019 Rui Caridade <rui.mcbc@gmail.com>
# Copyright (C) 2021 Matthew Lloyd <github@matthewlloyd.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import math
from . import probe
class ScrewsTiltAdjust:
def __init__(self, config):
self.config = config
self.printer = config.get_printer()
self.screws = []
self.results = []
self.max_diff = None
self.max_diff_error = False
# Read config
for i in range(99):
prefix = "screw%d" % (i + 1,)
if config.get(prefix, None) is None:
break
screw_coord = config.getfloatlist(prefix, count=2)
screw_name = "screw at %.3f,%.3f" % screw_coord
screw_name = config.get(prefix + "_name", screw_name)
self.screws.append((screw_coord, screw_name))
if len(self.screws) < 3:
raise config.error("screws_tilt_adjust: Must have "
"at least three screws")
self.threads = {'CW-M3': 0, 'CCW-M3': 1, 'CW-M4': 2, 'CCW-M4': 3,
'CW-M5': 4, 'CCW-M5': 5, 'CW-M6': 6, 'CCW-M6': 7}
self.thread = config.getchoice('screw_thread', self.threads,
default='CW-M3')
# Initialize ProbePointsHelper
points = [coord for coord, name in self.screws]
self.probe_helper = probe.ProbePointsHelper(self.config,
self.probe_finalize,
default_points=points)
self.probe_helper.minimum_points(3)
# Register command
self.gcode = self.printer.lookup_object('gcode')
self.gcode.register_command("SCREWS_TILT_CALCULATE",
self.cmd_SCREWS_TILT_CALCULATE,
desc=self.cmd_SCREWS_TILT_CALCULATE_help)
cmd_SCREWS_TILT_CALCULATE_help = "Tool to help adjust bed leveling " \
"screws by calculating the number " \
"of turns to level it."
def cmd_SCREWS_TILT_CALCULATE(self, gcmd):
self.max_diff = gcmd.get_float("MAX_DEVIATION", None)
# Option to force all turns to be in the given direction (CW or CCW)
direction = gcmd.get("DIRECTION", default=None)
if direction is not None:
direction = direction.upper()
if direction not in ('CW', 'CCW'):
raise gcmd.error(
"Error on '%s': DIRECTION must be either CW or CCW" % (
gcmd.get_commandline(),))
self.direction = direction
self.probe_helper.start_probe(gcmd)
def get_status(self, eventtime):
return {'error': self.max_diff_error,
'max_deviation': self.max_diff,
'results': self.results}
def probe_finalize(self, offsets, positions):
self.results = {}
self.max_diff_error = False
# Factors used for CW-M3, CCW-M3, CW-M4, CCW-M4, CW-M5, CCW-M5, CW-M6
#and CCW-M6
threads_factor = {0: 0.5, 1: 0.5, 2: 0.7, 3: 0.7, 4: 0.8, 5: 0.8,
6: 1.0, 7: 1.0}
is_clockwise_thread = (self.thread & 1) == 0
screw_diff = []
# Process the read Z values
if self.direction is not None:
# Lowest or highest screw is the base position used for comparison
use_max = ((is_clockwise_thread and self.direction == 'CW')
or (not is_clockwise_thread and self.direction == 'CCW'))
min_or_max = max if use_max else min
i_base, z_base = min_or_max(
enumerate([pos[2] for pos in positions]), key=lambda v: v[1])
else:
# First screw is the base position used for comparison
i_base, z_base = 0, positions[0][2]
# Provide the user some information on how to read the results
self.gcode.respond_info("01:20 means 1 full turn and 20 minutes, "
"CW=clockwise, CCW=counter-clockwise")
for i, screw in enumerate(self.screws):
z = positions[i][2]
coord, name = screw
if i == i_base:
# Show the results
self.gcode.respond_info(
"%s : x=%.1f, y=%.1f, z=%.5f" %
(name + ' (base)', coord[0], coord[1], z))
sign = "CW" if is_clockwise_thread else "CCW"
self.results["screw%d" % (i + 1,)] = {'z': z, 'sign': sign,
'adjust': '00:00', 'is_base': True}
else:
# Calculate how knob must be adjusted for other positions
diff = z_base - z
screw_diff.append(abs(diff))
if abs(diff) < 0.001:
adjust = 0
else:
adjust = diff / threads_factor.get(self.thread, 0.5)
if is_clockwise_thread:
sign = "CW" if adjust >= 0 else "CCW"
else:
sign = "CCW" if adjust >= 0 else "CW"
adjust = abs(adjust)
full_turns = math.trunc(adjust)
decimal_part = adjust - full_turns
minutes = round(decimal_part * 60, 0)
# Show the results
self.gcode.respond_info(
"%s : x=%.1f, y=%.1f, z=%.5f : adjust %s %02d:%02d" %
(name, coord[0], coord[1], z, sign, full_turns, minutes))
self.results["screw%d" % (i + 1,)] = {'z': z, 'sign': sign,
'adjust':"%02d:%02d" % (full_turns, minutes),
'is_base': False}
if self.max_diff and any((d > self.max_diff) for d in screw_diff):
self.max_diff_error = True
raise self.gcode.error(
"bed level exceeds configured limits ({}mm)! " \
"Adjust screws and restart print.".format(self.max_diff))
def load_config(config):
return ScrewsTiltAdjust(config)

72
files/scripts/useful_macros.sh Executable file
View file

@ -0,0 +1,72 @@
#!/bin/sh
set -e
function backup_klipper(){
if [ -f /usr/data/printer_data/config/backup_config.tar.gz ]; then
rm -f /usr/data/printer_data/config/backup_config.tar.gz
fi
cd /usr/data/printer_data
echo -e "Info: Compressing files..."
tar -czvf /usr/data/printer_data/config/backup_config.tar.gz config
echo -e "Info: Klipper configuration files have been saved successfully!"
exit 0
}
function restore_klipper(){
if [ ! -f /usr/data/printer_data/config/backup_config.tar.gz ]; then
echo -e "Info: Please backup Klipper configuration files before restore!"
exit 1
fi
cd /usr/data/printer_data
mv config/backup_config.tar.gz backup_config.tar.gz
if [ -d config ]; then
rm -rf config
fi
echo -e "Info: Restoring files..."
tar -xvf backup_config.tar.gz
mv backup_config.tar.gz config/backup_config.tar.gz
echo -e "Info: Klipper configuration files have been restored successfully!"
exit 0
}
function backup_moonraker(){
if [ -f /usr/data/printer_data/config/backup_database.tar.gz ]; then
rm -f /usr/data/printer_data/config/backup_database.tar.gz
fi
cd /usr/data/printer_data
echo -e "Info: Compressing files..."
tar -czvf /usr/data/printer_data/config/backup_database.tar.gz database
echo -e "Info: Moonraker database has been saved successfully!"
exit 0
}
function restore_moonraker(){
if [ ! -f /usr/data/printer_data/config/backup_database.tar.gz ]; then
echo -e "Info: Please backup Moonraker database before restore!"
exit 1
fi
cd /usr/data/printer_data
mv config/backup_database.tar.gz backup_database.tar.gz
if [ -d database ]; then
rm -rf database
fi
echo -e "Info: Restoring files..."
tar -xvf backup_database.tar.gz
mv backup_database.tar.gz config/backup_database.tar.gz
echo -e "Info: Moonraker database has been restored successfully!"
exit 0
}
if [ "$1" == "-backup_klipper" ]; then
backup_klipper
elif [ "$1" == "-restore_klipper" ]; then
restore_klipper
elif [ "$1" == "-backup_moonraker" ]; then
backup_moonraker
elif [ "$1" == "-restore_moonraker" ]; then
restore_moonraker
else
echo -e "Invalid argument. Usage: $0 [-backup_klipper | -restore_klipper | -backup_moonraker | -restore_moonraker]"
exit 1
fi

32
files/services/S50nginx Executable file
View file

@ -0,0 +1,32 @@
#!/bin/sh
#
# Start/stop nginx
#
NGINX=/usr/data/nginx/sbin/nginx
PIDFILE=/var/run/nginx.pid
NGINX_ARGS="-c /usr/data/nginx/nginx/nginx.conf"
case "$1" in
start)
echo "Starting nginx..."
mkdir -p /var/log/nginx /var/tmp/nginx
start-stop-daemon -S -p "$PIDFILE" --exec "$NGINX" -- $NGINX_ARGS
;;
stop)
echo "Stopping nginx..."
start-stop-daemon -K -x "$NGINX" -p "$PIDFILE" -o
;;
reload|force-reload)
echo "Reloading nginx configuration..."
"$NGINX" -s reload
;;
restart)
"$0" stop
sleep 1 # Prevent race condition: ensure nginx stops before start.
"$0" start
;;
*)
echo "Usage: $0 {start|stop|restart|reload|force-reload}"
exit 1
esac

View file

@ -0,0 +1,54 @@
#!/bin/sh
#
# Starts klipper service.
#
USER_DATA=/usr/data
PROG=/usr/share/klippy-env/bin/python
PY_SCRIPT=/usr/share/klipper/klippy/klippy.py
PRINTER_DATA_DIR=$USER_DATA/printer_data
PRINTER_CONFIG_DIR=$PRINTER_DATA_DIR/config
PRINTER_LOGS_DIR=$PRINTER_DATA_DIR/logs
PID_FILE=/var/run/klippy.pid
mcu_reset()
{
[ -z $(pidof klipper_mcu) ] || /etc/init.d/S57klipper_mcu restart
}
start() {
mcu_reset
HOME=/root start-stop-daemon -S -q -b -m -p $PID_FILE \
--exec $PROG -- $PY_SCRIPT \
$PRINTER_CONFIG_DIR/printer.cfg \
-l $PRINTER_LOGS_DIR/klippy.log \
-a /tmp/klippy_uds
}
stop() {
start-stop-daemon -K -q -p $PID_FILE
}
restart() {
stop
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
restart
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit $?

View file

@ -0,0 +1,51 @@
#!/bin/sh
#
# Starts moonraker service.
#
USER_DATA=/usr/data
PROG=/usr/data/moonraker/moonraker-env/bin/python
PY_SCRIPT=/usr/data/moonraker/moonraker/moonraker/moonraker.py
DEFAULT_CFG=/usr/data/moonraker//moonraker/moonraker.conf
PRINTER_DATA_DIR=$USER_DATA/printer_data
PRINTER_CONFIG_DIR=$PRINTER_DATA_DIR/config
PRINTER_LOGS_DIR=$PRINTER_DATA_DIR/logs
PID_FILE=/var/run/moonraker.pid
start() {
[ -d $PRINTER_DATA_DIR ] || mkdir -p $PRINTER_DATA_DIR
[ -d $PRINTER_CONFIG_DIR ] || mkdir -p $PRINTER_CONFIG_DIR
[ -d $PRINTER_LOGS_DIR ] || mkdir -p $PRINTER_LOGS_DIR
[ -s $PRINTER_CONFIG_DIR/moonraker.conf ] || cp $DEFAULT_CFG $PRINTER_CONFIG_DIR/moonraker.conf
rm -rf /usr/data/moonraker/tmp; mkdir -p /usr/data/moonraker/tmp
TMPDIR=/usr/data/moonraker/tmp HOME=/root start-stop-daemon -S -q -b -m -p $PID_FILE \
--exec $PROG -- $PY_SCRIPT -d $PRINTER_DATA_DIR
}
stop() {
start-stop-daemon -K -q -p $PID_FILE
}
restart() {
stop
sleep 1
start
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart|reload)
restart
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
esac
exit $?