I have been working on a klippy extras module for the 3DChameleon for a while now and would like to share it here. It's still a work in progress, but it makes the Chameleon (in my experience) more reliable as it uses your printer's filament runout sensor to detect failed loads and unloads (as well as assisting with loads and unloads)
Here is `3dchameleon.py`:
import logging
import time
class Chameleon:
def __init__(self, config):
self.printer = config.get_printer()
self.gcode = self.printer.lookup_object('gcode')
self.config = config
self.filament_sensor_name = config.get('filament_sensor_name', 'fsensor')
self.filament_sensor_type = config.get('filament_sensor_type', 'filament_switch_sensor')
self.fsensor = self.printer.load_object(self.config, f'{self.filament_sensor_type} {self.filament_sensor_name}')
self.pin = config.get('pin', '3dchameleon')
self.chameleon_pin = self.printer.load_object(self.config, f'output_pin {self.pin}')
self.unload_time = config.getfloat('unload_time', 7.5)
self.max_unload_time = config.getfloat('max_unload_time', 10)
self.load_time = config.getint('load_time', 0.5)
self.max_load_time = config.getfloat('max_load_time', self.load_time + 10)
self.pulse_time = config.getfloat('pulse_time', 0.5)
self.filament_detected = False
logging.info('3DChameleon: Initialized Successfully')
self.gcode.register_command(
'UPDATE_CHAMELEON_SENSOR',
self.cmd_UPDATE_CHAMELEON_SENSOR
)
self.gcode.register_command(
'SET_CHAMELEON',
self.cmd_SET_CHAMELEON,
self.cmd_SET_CHAMELEON_help
)
self.gcode.register_command(
'UNLOAD_CHAMELEON',
self.cmd_UNLOAD_CHAMELEON,
self.cmd_UNLOAD_CHAMELEON_help
)
self.gcode.register_command(
'LOAD_CHAMELEON',
self.cmd_LOAD_CHAMELEON,
self.cmd_LOAD_CHAMELEON_help
)
self.gcode.register_command(
'PRESS_CHAMELEON',
self.cmd_PRESS_CHAMELEON,
self.cmd_PRESS_CHAMELEON_help
)
self.gcode.register_command(
'PULSE_CHAMELEON',
self.cmd_PULSE_CHAMELEON,
self.cmd_PULSE_CHAMELEON_help
)
self.gcode.register_command(
'QUERY_CHAMELEON_SENSOR',
self.cmd_QUERY_CHAMELEON_SENSOR,
self.cmd_QUERY_CHAMELEON_SENSOR_help
)
self.printer.register_event_handler("klippy:ready", lambda: self.cmd_UPDATE_CHAMELEON_SENSOR(None))
def _read_fsensor(self):
return self.fsensor.runout_helper.filament_present
def _set_chameleon(self, value):
toolhead = self.printer.lookup_object('toolhead')
toolhead.register_lookahead_callback(
lambda print_time: self.chameleon_pin._set_pin(print_time, value))
def cmd_UPDATE_CHAMELEON_SENSOR(self, gcmd=None):
self.filament_detected = self._read_fsensor()
cmd_UNLOAD_CHAMELEON_help = 'Unloads the 3DChameleon until the filament is past the filament runout sensor (when it reads False), and then waits unload_time to pull the filament out of the way for the next filament. Note that if the filament takes more than max_unload_time to trigger the filament sensor, then it will abort'
def cmd_UNLOAD_CHAMELEON(self, gcmd):
self._set_chameleon(True)
start = time.time()
self.gcode.run_script_from_command('UPDATE_CHAMELEON_SENSOR')
while self.filament_detected:
logging.info(f'3DChameleon Unload Sensor: {self.filament_detected}')
self.gcode.run_script_from_command('UPDATE_CHAMELEON_SENSOR')
if time.time() - start > self.max_unload_time:
self._set_chameleon(False)
self.gcode.run_script_from_command('M117 Unload Failed')
self.gcode.run_script_from_command('PAUSE')
break
self.gcode.run_script_from_command('G4 P250')
self.gcode.run_script_from_command(f'G4 P{self.unload_time * 1000}')
self._set_chameleon(False)
cmd_LOAD_CHAMELEON_help = 'Loads the 3DChameleon until the filament is past the filament runout sensor (when it reads True), and then waits load_time to push the filament into the extruder. Note that if the filament takes more than max_load_time to trigger the filament sensor, then it will abort'
def cmd_LOAD_CHAMELEON(self, gcmd):
self._set_chameleon(True)
start = time.time()
self.gcode.run_script_from_command('UPDATE_CHAMELEON_SENSOR')
while not self.filament_detected:
logging.info(f'3DChameleon Load Sensor: {self.filament_detected}')
self.gcode.run_script_from_command('UPDATE_CHAMELEON_SENSOR')
if time.time() - start > self.max_load_time:
self._set_chameleon(False)
self.gcode.run_script_from_command('M117 Load Failed')
self.gcode.run_script_from_command('PAUSE')
break
self.gcode.run_script_from_command('G4 P250')
self.gcode.run_script_from_command(f'G4 P{self.load_time * 1000}')
self._set_chameleon(False)
cmd_PULSE_CHAMELEON_help = 'Presses the 3DChameleon for the duration of PULSES * pulse_time'
def cmd_PULSE_CHAMELEON(self, gcmd):
self._set_chameleon(True)
pulses = gcmd.get_int('PULSES', 0)
pulses_ms = int(pulses * self.pulse_time * 1000)
self.gcode.run_script_from_command(f'G4 P{pulses_ms}')
self._set_chameleon(False)
cmd_PRESS_CHAMELEON_help = 'Press the 3DChameleon for the duration of DURATION'
def cmd_PRESS_CHAMELEON(self, gcmd):
self._set_chameleon(True)
duration = gcmd.get_float('DURATION', 0)
duration_ms = int(duration * 1000)
self.gcode.run_script_from_command(f'G4 P{duration_ms}')
self._set_chameleon(False)
cmd_SET_CHAMELEON_help = 'Sets the 3DChameleon pin to HIGH or LOW, as determined by VALUE'
def cmd_SET_CHAMELEON(self, gcmd):
value = gcmd.get_int('VALUE', 0)
self._set_chameleon(value)
gcmd.respond_info(f'Set 3DChameleon = {value}')
cmd_QUERY_CHAMELEON_SENSOR_help = 'Returns the current state of the 3DChameleon filament sensor'
def cmd_QUERY_CHAMELEON_SENSOR(self, gcmd):
value = self._read_fsensor()
gcmd.respond_info(f'Sensor value: {value}')
def load_config(config):
return Chameleon(config)
And here is `3dchameleon.cfg` You'll want to include this in printer.cfg:
[output_pin 3dchameleon]
pin: host: gpio20
[3dchameleon]
pulse_time: 0.495
unload_time: 5.5
max_load_time: 25
[save_variables]
filename: ~/printer_data/config/variables.cfg
[gcode_macro Set_Chameleon_State]
gcode:
SAVE_VARIABLE VARIABLE=prev_ext VALUE={params.P|int}
SAVE_VARIABLE VARIABLE=layer VALUE={params.L|int}
[gcode_macro Get_Chameleon_State]
gcode:
{% set saved = printer.save_variables.variables %}
RESPOND TYPE=command MSG='P{saved.prev_ext} L{saved.layer}'
[gcode_macro Toolchange]
gcode:
{% set saved = printer.save_variables.variables %}
{% set t = params.T|int %}
{% set p = saved.prev_ext %}
{% set l = saved.layer %}
SAVE_VARIABLE VARIABLE=efsensor VALUE=0
M83
G92 E0
M117 T{p} -> T{t}
# G0 E-20 F500
PULSE_CHAMELEON PULSES={t+1}
UNLOAD_CHAMELEON
LOAD_CHAMELEON
{% if p>-1 %}
G92 E0
G0 E20 F2000 ; E-80
G90
{% endif %}
G92 E0
M83
G0 E20 F1500; <<<--- adjust this E value to tune extruder loading
G4 P400
G92 E0
G90
M83
M117 3D Chameleon Tool T{t}
SAVE_VARIABLE VARIABLE=prev_ext VALUE={t}
G4 P1000
SAVE_VARIABLE VARIABLE=efsensor VALUE=1
[gcode_macro T0]
gcode:
Toolchange T=0
[gcode_macro T1]
gcode:
Toolchange T=1
[gcode_macro T2]
gcode:
Toolchange T=2
[gcode_macro T3]
gcode:
Toolchange T=3
Finally, here is my custom toolchange GCode:
SET_CHAMELEON_STATE P={previous_extruder} L={layer_num}
T{next_extruder}
SET_CHAMELEON_STATE P={next_extruder} L={layer_num}
I'm still working out reliable start/end GCodes, but for now there are several commands in the extras module:
PULSE_CHAMELEON PULSES=x
PRESS_CHAMELEON DURATION=x (seconds)
LOAD_CHAMELEON (use the filament sensor to load plus extra)
UNLOAD_CHAMELEON (use the filament sensor to unload plus extra)
QUERY_CHAMELEON_SENSOR (read filament sensor)
SET_CHAMELEON VALUE=x (set pin high or low)
This depends on the Chameleon being attached to the Raspberry Pi via GPIO pin (with a relay), and you configure it by setting the [output_pin] to the correct pin, and then there are several configuration options for the 3DChameleon:
filament_sensor_name
filament_sensor_type
pin (pin name)
unload_time
max_unload_time
load_time
max_load_time
pulse_time (0.5s default)
NOTE: All times are in SECONDS, not ms.
This is still a WIP, so I wouldn't use this as your primary means of controlling your Chameleon.
Any ideas to further this would be appreciated.
I'm discontinuing this plugin due to the hardware issues I've been experiencing with the 3DChameleon. I have since disassembled it for parts and have used the motors to run the 3MS. The 3MS is a modular, fully open-source multimaterial system for Klipper. It has full documentation, and I don't make any money if you build one. If you want reliable Klipper multimaterial 3D printing, here is the link:
https://github.com/3DCoded/3MS
Hello there. I have tried using your latest implementation from GitHub. It doesn’t work in case no filament sensor is installed no?
I am currently modifying macros to work in a simpler manner and from there build on top and utilize the other features.
What I am trying now is to have a single macro that interfaces with 3d chameleon (instead of the utility functions) something like 'chameleon p=pulses d=duration s=secondaryduration' with 'G4 P500's. Still I have issues executing correct command (sometimes it has a buffer from a previous command). I am wondering if Reset_chameleon will make it forget the current filament in its buffer?
I just installed Klipper on my Ender 3 Pro. After a few days of struggling, I've finally got it operational and am trying to get your implementation working for the 3D Chameleon. My filament sensor though is the BTT Smart Filament Sensor. I haven't been able to figure out how to identify it in 3dchameleon.cfg. It's been a few years since I've done any realy programming, but I can usually follow along.
Wow... this is awesome!!!
Bill