Right. My variables.cfg includes (at the moment):
spool_registry = {'1': {'color': None, 'id': None, 'name': None}, '2': {'color': None, 'id': None, 'name': None}, '3': {'color': '3B7501', 'id': '21', 'name': 'Solutech PLA Green'}, '4': {'color': 'EC0000', 'id': '39', 'name': 'Elegoo PLA Red'}, '5': {'color': 'FFFFFF', 'id': '38', 'name': 'Elegoo PLA White'}, '6': {'color': 'A0A0A0', 'id': '31', 'name': 'Elegoo PLA Grey'}}
Here is a set of macros that are only used by KlipperScreen to reflect changes to spool_registry
made from my hacked-up KS panel:
[gcode_macro _UNASSIGN_SPOOL]
gcode:
{% set spool_registry = printer.save_variables.variables.spool_registry %}
{% set slot = params.SLOT %}
{% for param in spool_registry[slot] %}
{% set dummy = spool_registry[slot].__setitem__(param, None) %}
{% endfor %}
SAVE_VARIABLE VARIABLE=spool_registry VALUE="{spool_registry | pprint | replace("\n", "") | replace("\"", "\\\"")}"
_SET_TOOL_COLOR TOOL={slot|int - 1} COLOR=None
[gcode_macro _ASSIGN_SPOOL]
gcode:
{% set spool_registry = printer.save_variables.variables.spool_registry %}
{% set slot = params.SLOT %}
{% set spool_id = params.SPOOL_ID %}
{% set spool_name = printer['gcode_shell_command spoolman_client'].last_output | replace("\n", "") %}
{% set dummy = spool_registry[slot].__setitem__('id', spool_id) %}
{% set dummy = spool_registry[slot].__setitem__('name', spool_name) %}
{% set color = params.COLOR|default(None) %}
{% set dummy = spool_registry[slot].__setitem__('color', color) %}
SAVE_VARIABLE VARIABLE=spool_registry VALUE="{spool_registry | pprint | replace("\n", "") | replace("\"", "\\\"")}"
_SET_TOOL_COLOR TOOL={slot|int - 1} COLOR={color}
[gcode_macro SPOOLS]
variable_slot: None
variable_spool_id: None
gcode:
; Args: SLOT, SPOOL_ID, COLOR, REMOVE
{% set spool_registry = printer.save_variables.variables.spool_registry %}
{% set globals = printer["gcode_macro GLOBALS"] %}
{% set slot = params.SLOT %}
{% if params.REMOVE %}
_UNASSIGN_SPOOL SLOT={slot}
{% else %}
{% set spool_id = params.SPOOL_ID %}
{% for slot in spool_registry %}
{% if spool_registry[slot]['id']|int == spool_id|int %}
_UNASSIGN_SPOOL SLOT={slot}
{% endif %}
{% endfor %}
RUN_SHELL_COMMAND CMD=spoolman_client PARAMS={params.SPOOL_ID}
_ASSIGN_SPOOL SPOOL_ID={params.SPOOL_ID} SLOT={slot} COLOR={params.COLOR|default(None)}
{% endif %}
# Dummy argument block for Mainsail
{% set dummy = None if True else "
{% set dummy = params.SLOT|default(None)|int %}
{% set dummy = params.SPOOL_ID|default(None)|int %}
{% set dummy = params.REMOVE|default(False)|bool %}
" %} # End argument block for Mainsail
Then I call these macros from my PRINT_START
and FILAMENT_CHANGE
macros:
[gcode_macro SET_ACTIVE_SPOOL]
gcode:
{% if params.ID %}
{% set id = params.ID|int %}
{action_call_remote_method(
"spoolman_set_active_spool",
spool_id=id
)}
{action_respond_info("Spool %s is now active."|format(id))}
{% else %}
{action_respond_info("Parameter 'ID' is required")}
{% endif %}
[gcode_macro _SET_ACTIVE_SPOOL]
gcode:
{% set spool_registry = printer.save_variables.variables.spool_registry %}
{% set globals = printer["gcode_macro GLOBALS"] %}
{% if globals.active_slot is not none %}
{% set id = spool_registry["%s"|format(globals.active_slot)]['id'] %}
SET_ACTIVE_SPOOL ID={id}
{% else %}
{action_respond_info("No slot selected. Could not set active spool.")}
{% endif %}
[gcode_macro CLEAR_ACTIVE_SPOOL]
gcode:
{action_call_remote_method(
"spoolman_set_active_spool",
spool_id=None
)}
[gcode_macro _CHECK_FOR_REQUESTED_FILAMENT]
gcode:
{% set name = params.NAME %}
{% set globals = printer["gcode_macro GLOBALS"] %}
{% set spool_registry = printer.save_variables.variables.spool_registry %}
{% set filament_list = [] %}
{% for slot in spool_registry %}
{% set dummy = filament_list.append(spool_registry[slot]['name']) %}
{% endfor %}
{% if name in filament_list %}
{ action_respond_info('Requested filament "%s" found.'|format(name))}
{% else %}
{ action_raise_error('Load "%s" filament in a spool slot and try again!'|format(name)) }
{% endif %}
Oh, I almost forgot. This also depends on the shell_command addon for Klipper and this block in printer.cfg:
[gcode_shell_command spoolman_client]
command: python3 /home/pi/spoolman-python-client/main.py get_spool --id
timeout: 30.
verbose: True
…and it’s a version of gcode_shell_command.py that I tweaked to make the output of the shell command accessible to Klipper via printer['gcode_shell_command spoolman_client'].last_output
:
# 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.last_output = None
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)
self.last_output = 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 get_status(self, eventtime):
return {'last_output': self.last_output}
def load_config_prefix(config):
return ShellCommand(config)
Then I have an overengineered python script that takes a spool ID as an argument and returns the name of the filament from Spoolman, which Klipper uses to populate the spool_registry
and to check whether the filament requested by the slicer is loaded into a slot (since the slicer doesn’t know or care about spool IDs and it shouldn’t matter as long as some spool of the correct filament is loaded somewhere).