2018-05-25 18:33:01 +02:00
|
|
|
# Add ability to define custom g-code macros
|
|
|
|
#
|
2021-01-15 04:13:50 +01:00
|
|
|
# Copyright (C) 2018-2021 Kevin O'Connor <kevin@koconnor.net>
|
2018-05-25 18:33:01 +02:00
|
|
|
#
|
|
|
|
# This file may be distributed under the terms of the GNU GPLv3 license.
|
2021-01-15 04:13:50 +01:00
|
|
|
import traceback, logging, ast, copy
|
2019-01-22 20:35:32 +01:00
|
|
|
import jinja2
|
2018-05-25 18:33:01 +02:00
|
|
|
|
2019-01-22 20:35:32 +01:00
|
|
|
|
|
|
|
######################################################################
|
|
|
|
# Template handling
|
|
|
|
######################################################################
|
|
|
|
|
2019-05-28 16:59:12 +02:00
|
|
|
# Wrapper for access to printer object get_status() methods
|
|
|
|
class GetStatusWrapper:
|
2019-01-22 20:35:32 +01:00
|
|
|
def __init__(self, printer, eventtime=None):
|
|
|
|
self.printer = printer
|
|
|
|
self.eventtime = eventtime
|
|
|
|
self.cache = {}
|
|
|
|
def __getitem__(self, val):
|
|
|
|
sval = str(val).strip()
|
|
|
|
if sval in self.cache:
|
|
|
|
return self.cache[sval]
|
|
|
|
po = self.printer.lookup_object(sval, None)
|
|
|
|
if po is None or not hasattr(po, 'get_status'):
|
|
|
|
raise KeyError(val)
|
|
|
|
if self.eventtime is None:
|
|
|
|
self.eventtime = self.printer.get_reactor().monotonic()
|
2021-01-15 04:13:50 +01:00
|
|
|
self.cache[sval] = res = copy.deepcopy(po.get_status(self.eventtime))
|
2019-01-22 20:35:32 +01:00
|
|
|
return res
|
2019-06-04 18:41:28 +02:00
|
|
|
def __contains__(self, val):
|
|
|
|
try:
|
|
|
|
self.__getitem__(val)
|
|
|
|
except KeyError as e:
|
|
|
|
return False
|
|
|
|
return True
|
2019-06-30 13:11:45 +02:00
|
|
|
def __iter__(self):
|
|
|
|
for name, obj in self.printer.lookup_objects():
|
|
|
|
if self.__contains__(name):
|
|
|
|
yield name
|
2019-01-22 20:35:32 +01:00
|
|
|
|
|
|
|
# Wrapper around a Jinja2 template
|
|
|
|
class TemplateWrapper:
|
|
|
|
def __init__(self, printer, env, name, script):
|
|
|
|
self.printer = printer
|
|
|
|
self.name = name
|
|
|
|
self.gcode = self.printer.lookup_object('gcode')
|
2020-08-16 21:39:30 +02:00
|
|
|
gcode_macro = self.printer.lookup_object('gcode_macro')
|
|
|
|
self.create_template_context = gcode_macro.create_template_context
|
2019-01-22 20:35:32 +01:00
|
|
|
try:
|
|
|
|
self.template = env.from_string(script)
|
|
|
|
except Exception as e:
|
|
|
|
msg = "Error loading template '%s': %s" % (
|
|
|
|
name, traceback.format_exception_only(type(e), e)[-1])
|
|
|
|
logging.exception(msg)
|
|
|
|
raise printer.config_error(msg)
|
|
|
|
def render(self, context=None):
|
|
|
|
if context is None:
|
2020-08-16 21:39:30 +02:00
|
|
|
context = self.create_template_context()
|
2019-01-22 20:35:32 +01:00
|
|
|
try:
|
|
|
|
return str(self.template.render(context))
|
|
|
|
except Exception as e:
|
|
|
|
msg = "Error evaluating '%s': %s" % (
|
|
|
|
self.name, traceback.format_exception_only(type(e), e)[-1])
|
|
|
|
logging.exception(msg)
|
|
|
|
raise self.gcode.error(msg)
|
|
|
|
def run_gcode_from_command(self, context=None):
|
|
|
|
self.gcode.run_script_from_command(self.render(context))
|
|
|
|
|
|
|
|
# Main gcode macro template tracking
|
|
|
|
class PrinterGCodeMacro:
|
|
|
|
def __init__(self, config):
|
|
|
|
self.printer = config.get_printer()
|
|
|
|
self.env = jinja2.Environment('{%', '%}', '{', '}')
|
2019-06-07 16:51:30 +02:00
|
|
|
def load_template(self, config, option, default=None):
|
2019-01-22 20:35:32 +01:00
|
|
|
name = "%s:%s" % (config.get_name(), option)
|
2019-06-07 16:51:30 +02:00
|
|
|
if default is None:
|
|
|
|
script = config.get(option)
|
|
|
|
else:
|
|
|
|
script = config.get(option, default)
|
2019-01-22 20:35:32 +01:00
|
|
|
return TemplateWrapper(self.printer, self.env, name, script)
|
2020-08-16 21:39:30 +02:00
|
|
|
def _action_emergency_stop(self, msg="action_emergency_stop"):
|
|
|
|
self.printer.invoke_shutdown("Shutdown due to %s" % (msg,))
|
|
|
|
return ""
|
|
|
|
def _action_respond_info(self, msg):
|
|
|
|
self.printer.lookup_object('gcode').respond_info(msg)
|
|
|
|
return ""
|
|
|
|
def _action_raise_error(self, msg):
|
|
|
|
raise self.printer.command_error(msg)
|
2020-10-28 13:42:17 +01:00
|
|
|
def _action_call_remote_method(self, method, **kwargs):
|
|
|
|
webhooks = self.printer.lookup_object('webhooks')
|
|
|
|
try:
|
|
|
|
webhooks.call_remote_method(method, **kwargs)
|
|
|
|
except self.printer.command_error:
|
|
|
|
logging.exception("Remote Call Error")
|
|
|
|
return ""
|
2020-08-16 21:39:30 +02:00
|
|
|
def create_template_context(self, eventtime=None):
|
|
|
|
return {
|
|
|
|
'printer': GetStatusWrapper(self.printer, eventtime),
|
|
|
|
'action_emergency_stop': self._action_emergency_stop,
|
|
|
|
'action_respond_info': self._action_respond_info,
|
|
|
|
'action_raise_error': self._action_raise_error,
|
2020-10-28 13:42:17 +01:00
|
|
|
'action_call_remote_method': self._action_call_remote_method,
|
2020-08-16 21:39:30 +02:00
|
|
|
}
|
2019-01-22 20:35:32 +01:00
|
|
|
|
|
|
|
def load_config(config):
|
|
|
|
return PrinterGCodeMacro(config)
|
|
|
|
|
|
|
|
|
|
|
|
######################################################################
|
|
|
|
# GCode macro
|
|
|
|
######################################################################
|
2018-09-27 18:08:01 +02:00
|
|
|
|
2018-05-25 18:33:01 +02:00
|
|
|
class GCodeMacro:
|
|
|
|
def __init__(self, config):
|
2021-05-26 19:03:10 +02:00
|
|
|
if len(config.get_name().split()) > 2:
|
|
|
|
raise config.error(
|
|
|
|
"Name of section '%s' contains illegal whitespace"
|
|
|
|
% (config.get_name()))
|
2019-05-28 18:17:08 +02:00
|
|
|
name = config.get_name().split()[1]
|
|
|
|
self.alias = name.upper()
|
2020-02-13 02:34:20 +01:00
|
|
|
self.printer = printer = config.get_printer()
|
2020-05-05 20:10:30 +02:00
|
|
|
gcode_macro = printer.load_object(config, 'gcode_macro')
|
2019-01-22 20:35:32 +01:00
|
|
|
self.template = gcode_macro.load_template(config, 'gcode')
|
2018-05-25 18:33:01 +02:00
|
|
|
self.gcode = printer.lookup_object('gcode')
|
2020-02-13 02:34:20 +01:00
|
|
|
self.rename_existing = config.get("rename_existing", None)
|
2021-05-26 20:21:21 +02:00
|
|
|
self.cmd_desc = config.get("description", "G-Code macro")
|
2020-02-13 02:34:20 +01:00
|
|
|
if self.rename_existing is not None:
|
|
|
|
if (self.gcode.is_traditional_gcode(self.alias)
|
|
|
|
!= self.gcode.is_traditional_gcode(self.rename_existing)):
|
|
|
|
raise config.error(
|
|
|
|
"G-Code macro rename of different types ('%s' vs '%s')"
|
|
|
|
% (self.alias, self.rename_existing))
|
|
|
|
printer.register_event_handler("klippy:connect",
|
|
|
|
self.handle_connect)
|
|
|
|
else:
|
|
|
|
self.gcode.register_command(self.alias, self.cmd,
|
|
|
|
desc=self.cmd_desc)
|
2019-05-28 18:17:08 +02:00
|
|
|
self.gcode.register_mux_command("SET_GCODE_VARIABLE", "MACRO",
|
|
|
|
name, self.cmd_SET_GCODE_VARIABLE,
|
|
|
|
desc=self.cmd_SET_GCODE_VARIABLE_help)
|
2018-05-25 18:33:01 +02:00
|
|
|
self.in_script = False
|
2019-01-22 20:35:32 +01:00
|
|
|
prefix = 'default_parameter_'
|
|
|
|
self.kwparams = { o[len(prefix):].upper(): config.get(o)
|
|
|
|
for o in config.get_prefix_options(prefix) }
|
2019-06-08 01:30:17 +02:00
|
|
|
self.variables = {}
|
2019-05-28 18:17:08 +02:00
|
|
|
prefix = 'variable_'
|
2019-06-08 01:30:17 +02:00
|
|
|
for option in config.get_prefix_options(prefix):
|
|
|
|
try:
|
|
|
|
self.variables[option[len(prefix):]] = ast.literal_eval(
|
|
|
|
config.get(option))
|
|
|
|
except ValueError as e:
|
|
|
|
raise config.error(
|
|
|
|
"Option '%s' in section '%s' is not a valid literal" % (
|
|
|
|
option, config.get_name()))
|
2020-02-13 02:34:20 +01:00
|
|
|
def handle_connect(self):
|
|
|
|
prev_cmd = self.gcode.register_command(self.alias, None)
|
|
|
|
if prev_cmd is None:
|
|
|
|
raise self.printer.config_error(
|
|
|
|
"Existing command '%s' not found in gcode_macro rename"
|
|
|
|
% (self.alias,))
|
|
|
|
pdesc = "Renamed builtin of '%s'" % (self.alias,)
|
|
|
|
self.gcode.register_command(self.rename_existing, prev_cmd, desc=pdesc)
|
|
|
|
self.gcode.register_command(self.alias, self.cmd, desc=self.cmd_desc)
|
2019-05-28 18:17:08 +02:00
|
|
|
def get_status(self, eventtime):
|
2021-01-15 04:13:50 +01:00
|
|
|
return self.variables
|
2019-05-28 18:17:08 +02:00
|
|
|
cmd_SET_GCODE_VARIABLE_help = "Set the value of a G-Code macro variable"
|
2020-04-25 04:26:43 +02:00
|
|
|
def cmd_SET_GCODE_VARIABLE(self, gcmd):
|
|
|
|
variable = gcmd.get('VARIABLE')
|
|
|
|
value = gcmd.get('VALUE')
|
2019-05-28 18:17:08 +02:00
|
|
|
if variable not in self.variables:
|
|
|
|
if variable in self.kwparams:
|
|
|
|
self.kwparams[variable] = value
|
|
|
|
return
|
2020-04-25 04:26:43 +02:00
|
|
|
raise gcmd.error("Unknown gcode_macro variable '%s'" % (variable,))
|
2019-06-08 01:30:17 +02:00
|
|
|
try:
|
|
|
|
literal = ast.literal_eval(value)
|
|
|
|
except ValueError as e:
|
2020-04-25 04:26:43 +02:00
|
|
|
raise gcmd.error("Unable to parse '%s' as a literal" % (value,))
|
2019-06-08 01:30:17 +02:00
|
|
|
self.variables[variable] = literal
|
2020-04-22 18:40:32 +02:00
|
|
|
def cmd(self, gcmd):
|
2018-05-25 18:33:01 +02:00
|
|
|
if self.in_script:
|
2020-04-22 18:40:32 +02:00
|
|
|
raise gcmd.error("Macro %s called recursively" % (self.alias,))
|
|
|
|
params = gcmd.get_command_parameters()
|
2018-09-27 18:08:01 +02:00
|
|
|
kwparams = dict(self.kwparams)
|
|
|
|
kwparams.update(params)
|
2019-05-28 18:17:08 +02:00
|
|
|
kwparams.update(self.variables)
|
2020-08-16 21:39:30 +02:00
|
|
|
kwparams.update(self.template.create_template_context())
|
2019-01-22 20:35:32 +01:00
|
|
|
kwparams['params'] = params
|
2018-05-25 18:33:01 +02:00
|
|
|
self.in_script = True
|
|
|
|
try:
|
2019-01-22 20:35:32 +01:00
|
|
|
self.template.run_gcode_from_command(kwparams)
|
2018-05-25 18:33:01 +02:00
|
|
|
finally:
|
|
|
|
self.in_script = False
|
|
|
|
|
|
|
|
def load_config_prefix(config):
|
|
|
|
return GCodeMacro(config)
|