Making a GCode controlled Toaster Oven with Klipper and Octoprint.
Patch to disable some pin configuration checks on klipper master at commit bf6f84b82d5942cf8a854a39a88826e7d11e1be9
x-patch - 2.21 kB - 05/14/2020 at 01:49
This is not a complete reflow oven building guide. There are great guides out there that go over all the details of how to take an oven from stock to functional. You should read them, many have great advice. Neither is it a guide to take you from zero to hero with klipper. Klipper is well documented though prior knowledge of klipper is helpful.
This is a log of how I got klipper to work with my proof of concept reflow oven. Not everything is detailed, e.g. my particular arduino hardware implementation, but if you are familiar with arduino, it's a pretty simple build. I also included things which I haven't seen anyone do with an oven before, like adding the halogen lamps, and
Not everything is perfect, and my oven is far from finished, but the perfect is the enemy of the good, so I put it out there for others to learn from and improve upon. Towards that, I wasn't sure this was going to work, so I didn't spend much time on doing things the "right" way, and instead, hacked it just to see if it would work. Turns out, that cut down on my development time, and I knocked this project out pretty quick. Since it has been functional, I have been keeping it pretty busy and hadn't wanted to take it out of service to work on it.
That said, here's some things that could have been done better.
I'm sure a klipper "extras" python module could have been written to handle the multiple heaters only having one associated thermal sensor. Likewise, an "extras" module could have been written to handle one heater having multiple pid loops.
My current oven does have a fan, but it's not controlled by the software of the arduino. I have an SSR on the way, I'd like the fan to be switchable off (trivial in klipper) and PWM controlled. I just don't have confidence that running the fan at full blast for all profiles isn't going to wear out the consumer-grade fan. On the plus side, having a fan meant very, very uniform temperatures, I couldn't measure more than 1C difference anywhere in the oven that was at least an inch from a heater. Lowering the fan speed might also help the oven hold on to some of the heat.
My oven needs more insulation. My oven, even with almost 20A of heat, could only push 1.4C per second. I'll have to add some of that aluminum engine heat barrier that everyone recommends. To deal with an oven that is barely adequate, I had to add some python code that would basically just max out the heat until the setpoint is reached. I would like to eliminate that as it doesn't allow me to do extremely tightly controlled reflow profiles. 2C per second is likely the minimum.
A servo or stepper controlled door opener would be really nice, maybe even with an exhaust fan. I haven't settled on a design yet, I'm still kicking around some ideas.
The python gcode generation scripts could use some cleanup. To be fair, they are pretty complete for basic oven control. If I think of more complicated things to do with the oven, it could use a rewrite.
I'm sure I'll think of some other enhancements, but these would definitely push the oven from "functional prototype" to a more "finished project" IMHO.
Good luck with your project!
You might think, as did I, that now would be the easy part. Write some 5-10 line gcode scripts, and you are good to go, right? Not exactly.
It all revolves around the "G4" command, dwell, and how OctoPrint handles it. By default, OctoPrint basically stops for the duration of the dwell command. It doesn't update the temperature graph, even when klipper reports back temperatures. That's not ideal for monitoring progress and tuning.
The alternative is to uncheck the box "Actively pause communication during G4 dwell command" in the OctoPrint settings, but that causes OctoPrint to go directly to the next line of the gcode script, effectively eliminating the pause.
As a solution, I wrote a small python script to generate the desired behavior, chopping longer waits into one second G4 calls with a M105 in between to update the temperature status.
def print_dwell_gcode(time, increment): print ";begin dwell" print ";total time:", time, "seconds" print ";increments:", increment for i in range(0,time//increment): print "M105" print "G4 P" + str(increment*1000) print ";end dwell" print ";" return
Pretty simple really, and the outcome is exactly as desired. The oven holds at the temperature (assigned before) and updates the temperature graph.
This and ramping up the temperature slowly the the basis for most of my use cases. So let's see the ramp up code.
def measured_ramp_gcode(time, increment, startC, endC, heaterName): deltaTemp = endC - startC increments = time / increment deltaPerIncrement = deltaTemp / float(increments) print ";begin measured ramp" print ";" +str(time), "seconds in", increment, "second increments for", increments, "increments" print ";start at", str(startC)+"C end at", str(endC)+"C" print ";"+str(deltaTemp/float(time))+"C per second ramp for", str(deltaTemp)+"C" for i in range(0,time//increment): print "SET_HEATER_TEMPERATURE HEATER="+ str(heaterName), \ "TARGET="+"%.3f" % (startC + deltaPerIncrement*(i+1) ) #print "G4 P" + str(increment*1000) for j in range(0, increment): # one per second print "M105" print "G4 P1000" print ";end measured ramp from", str(startC)+"C to", str(endC)+"C" print ";" return
This ramps up the temperature over a given time, inserting the necessary waits and temperature checks in between. You might think that updating the heater temperature more frequently would result in a smoother temperature ramp, but in my testing, that had an unintended effect. The PID controller, seeing the setpoint was nearby, would not turn up the heaters enough, and the measured temperature would end up lagging the setpoint. Setting the heater update increment to 5 to 10 seconds resulted in the temperatures more accurately following the desired ramp curve.
Finally, adding a piezo buzzer became necessary, as some use cases, e.g. preheating a mobile phone for screen removal, require immediate notification and action before the object heated cools. So we simply add this:
def ring_buzzer(seconds, pin): print ";begin ring buzzer" print ";total time:", seconds, "seconds" print "SET_PIN PIN="+pin, "VALUE=1" for i in range(0, seconds): print "M105" print "G4 P1000" print "SET_PIN PIN="+pin, "VALUE=0" print ";end ring buzzer" print ";"
The called pin needs to be set up in the configuration file.
Pulling it all together, this script to dehydrate ABS, starts by ramping the temperature to 65C over three minutes. Then it dwells for three hours at temperature. Finally, it rings the buzzer and turns off the heating elements.
#!/usr/bin/env python2 # import oven print ";start dehydrateABS" oven.start_gcode() print "setup_low_halogen" print "G4 P1000" oven.measured_ramp_gcode(60*3,10,32,65,"lowhalogen") oven.dwell_gcode(60*60*3,1) oven.ring_buzzer(10, "buzzer_pin") oven.end_gcode() print ";end dehydrateABS"
That 11 line script generates a 290 line gcode file, and works great...Read more »
So far, we have an oven, one heater configured, PID tuned and ready to go. It's able to hold steady at low temperatures for use cases like dehydrating 3d printing filament. But we have more heaters, and we need to also use those for other uses like reflow. Let's add to our configuration and use all the heaters at once. (Hell yeah!)
First we add the other mechanical relay module pins to the config. This should look familiar from earlier.
[output_pin resistive_top] pin: ar2 pwm: false value: 1 shutdown_value: 1 hardware_pwm: false [output_pin halogen_bottom] pin: ar8 pwm: false value: 1 shutdown_value: 1 hardware_pwm: false [output_pin resistive_bottom] pin: ar7 pwm: false value: 1 shutdown_value: 1 hardware_pwm: false
Also a gcode macro to handle configuring the pins
[0 SET_PIN PIN=resistive_top VALUE=0 SET_PIN PIN=halogen_bottom VALUE=0 SET_PIN PIN=halogen_top VALUE=0 G4 P500] gcode: TURN_OFF_HEATERS SET_PIN PIN=resistive_bottom VALUE=
Remember, low means on when using some of these relay modules.
Now on to add a new "generic_heater".
[heater_generic fullhalogenresistive] gcode_id: T1 heater_pin: ar5 sensor_type: MAX6675 sensor_pin: PB2 spi_software_sclk_pin: PB5 #CHRIS: hardware spi requires mosi pin on chip that has none spi_software_mosi_pin: PB3 spi_software_miso_pin: PB4 spi_speed: 100000 control = pid pid_Ki=2.047 pid_Kd=352.665 pid_Kp=200 min_temp: 0 max_temp: 300
Note the gcode_id of T1. We are specifying that this heater is "tool 1" instead of "tool 0" like the prior one. This allows us to graph it in OctoPrint, after editing the "Printer Configuration" and specifying that the machine has two tools/hotends. The graph can get a bit messy if you add many tools, so only add what you think you need.
Don't forget to add a "verify_heater" section, though at full blast, you may be OK with the default settings.
Now, if you reload, klipper will complain that your configuration is invalid, because you are sharing pins with your temperature sensor and your heater. This is an valid safety feature, that I disabled.
Here be dragons.
Klipper has a feature implemented whereby some devices can share a pin. I took advantage of this, and changed five lines to get this working.
My local git of klipper is at commit bf6f84b82d5942cf8a854a39a88826e7d11e1be9 and here is my patch that disables the pin sharing for heaters and the SPI bus. The Klipper code base moves VERY fast and I wouldn't even guarantee that this patch would work if you did a git clone as of the time of this writing.
Depending on your sensor selection (anything not SPI) this might not work for you. But the fix is pretty simple, read the code backtrace when klipper gives the error message.
Ok, since we have thoroughly voided our warranty, what can we now do?
We can now set up multiple pid settings for different heater configurations, which is essential. You shouldn't use the pid parameters for one heater when you turn on all the heaters.
We can also set up different pid loops for different situations. The most sailent situation I can think of is if you have a particularly touchy chip, you could use two different PID loops on the same set of heaters, one for aggressive heating, and another more conservative PID loop for holding at a temperature.
So, if you have followed along so far, you hopefully have a oven that has basic functionality. But that's not where it ends, we can do much more with some additional features and tuning.
For that, we need to take an aside and talk about octoprint a bit. Some basic configuration. In the GCODE section of octoprint settings, enter TURN_OFF_HEATERS in each of the three boxes as shown. This will turn off the heaters in the event of a cancellation, or if you forget to do so in your gcode.
I also turn off the GCODE Visualizer, it's not needed for oven use.
Next, let's install the OctoKlipper plugin. It's very handy when using klipper and has some buttons for codes you would otherwise have to remember or type. It is available in the "Plugin Manager" section of the OctoPrint Settings screen. Here's a screenshot of the OctoKlipper tab.
I also really like the OctoPrint-Tempsgraph plugin. It allows you to change up the temperature graph on the octoprint main page. You can zoom, pan, save a png, etc. Very useful, especially when PID tuning when normally you would see this:
Hey, looks pretty stable there! But you can zoom in and see this instead:
Looks like I could use a smidge more PID tuning! Actually, at this level of oscillation, it's hard to tell if that's really more PID tuning needed, just some noise on the thermocouple or just the thermal mass of the oven in relation to the wattage of the heater. I may or may not PID tune further, this level of accuracy is entirely acceptable for my use. But OctoPrint-Tempsgraph is still a very useful plugin.
Speaking of which, let's PID tune our heater!
If we go into the main klipper tab, we see a handy screen that prompts us for the heater name and target temperature.
I have set it up for a low target and run the test. Hopping over to the temperature tab, when the test completes, we see something like this.
Klipper reaches the setpoint, adjusts the temperatures, measures the reactions and suggests some PID parameters. You can see them in the Klipper tab. You can try them out by pasting them into the heater_generic section of the printer.cfg file we edited earlier. It should look something like this:
control = pid pid_Kp=62.338 pid_Ki=3.033 pid_Kd=320.260
You will need to restart klipper to see the effects. If you are lucky, you can run a heater test (shown earlier) and you will get an output like this:
Hey, that overshot just a smidge and leveled out quickly. I'm not sure I'd want to mess with that. If you aren't as fortunate, and the temperature floats above or below the set point, or oscillates unacceptably, let me be the first to welcome you to PID hell. You will have to manually tune the PID parameters. There are many documents that describe this arcane process, one I have found helpful was this: https://797ib1mbyf481ftl3rimdn3x-wpengine.netdna-ssl.com/wp-content/uploads/2012/09/How-to-Tune-PID-Loops-Control-Design-08222016.pdf
There's extra points if you find the mistake in the second paragraph of the "Answers" section.
I may write on the process I used to tune my PID parameters, we'll see.
This post assumes you have installed klipper and octoprint by following the instructions here:
It also assumes you have some familiarity with klipper already. If you haven't used klipper before, this might not be the project to start on.
We will start with a basic configuration, with one heater configured.
For my host, I used a Raspberry Pi Zero W. This is specifically recommended against, because it's too slow to feed commands to the MCU without stalling. It works just fine on my oven, as my gcode scripts only feed a few commands per second. I'm not running any steppers or extruders, so klipper is running at a slight fraction of normal speeds. Octoprint runs a bit slowly, but it's adequate.
My MCU is an arduino uno clone with a protoshield. The protoshield has a MAX6675 type-k thermocouple SPI chip and screw down connectors for the thermocouple. The protoshield also controls the SSR, the 4 relay module and a buzzer.
First, let's set up the active buzzer. This goes in the printer.cfg on the host. I used pin ar6 on my arduino for the buzzer.
[output_pin buzzer_pin] pin: ar6 pwm: false hardware_pwm: false value: 0 shutdown_value: 0
After reloading klipper so it pulls the new config, you can go into the octoprint terminal and test out the buzzer.
SET_PIN PIN=buzzer_pin VALUE=1
Set VALUE=0 to turn it off.
Now to set up the mechanical relays. I used pin ar8 for the mechanical relay connected to the bottom halogen lamp. Note the value and shutdown_value is set to 1. These optoisolated modules are often set up to make on low signal and turn off on high. This prevents the relays from making if the system reboots. I used four similar sections, one for each mechanical relay. You can test with the same command above, changing the PIN name and the VALUE.
[output_pin halogen_bottom] pin: ar8 pwm: false value: 1 shutdown_value: 1 hardware_pwm: false
Next, I set up some macros to make setting up the mechanical relays easy and quick.
[1 SET_PIN PIN=resistive_top VALUE=1 SET_PIN PIN=halogen_bottom VALUE=0 SET_PIN PIN=halogen_top VALUE=1 G4 P500] gcode: TURN_OFF_HEATERS SET_PIN PIN=resistive_bottom VALUE=
The gcode macro turns off all heaters (the SSR, more on that later) and set the unused heaters to 1. The heater we intend to use is set to 0. Then we stall for 0.5 seconds while the relays make or break.
Now to set up the SSR and thermocouple.
[heater_generic lowhalogen] gcode_id: T0 heater_pin: ar5 sensor_type: MAX6675 sensor_pin: PB2 spi_software_sclk_pin: PB5 spi_software_mosi_pin: PB3 spi_software_miso_pin: PB4 spi_speed: 100000 control = pid pid_ki = 1.269 pid_kp = 400 pid_kd = 60
I used an SPI type-k thermocouple converter chip, the MAX6675. Klipper does not support software SPI, so you will have to use the hardware SPI pins on your MCU. Many other sensor types are available, check the klipper docs. Next, the SSR is set up on my arduino at pin ar5. Technically, pin ar5 is hooked to a few resistors, an optocoupler and a JST jack to make removal easy. Tutorials on how to do this are available online.
For each heater_generic you set up, you need to set up a "verify_heater" section.
#[verify_heater lowhalogen] #max_error: 120 #check_gain_time: #hysteresis: 5 #heating_gain: 2
I have shown this, commented out, and with default values. This is a safety feature of klipper, and if not set up, will send klipper into emergency stop mode. Ovens don't heat as fast as 3d printer hotends do, and klipper, sensing that, will think something is wrong and will go into emergency shutdown mode. Check the example-extras.cfg file in klipper for documentation on how to set it up properly. My oven takes about 5 seconds to start seeing the temperature...Read more »
I had ordered 3 SSR right before CNY. Then covid happened, and I didn't think I was going to see my SSRs any time soon. So I made alternate plans, and I actually like this better than the original plan.
I use one high quality, Carlo Gavazzi, SSR to high speed switch all my heaters with PID control. But I get independent heater selection by also using a 4 relay module.
But you can't really use PID control with relays! That's right, you definitely shouldn't, as mechanical relays don't switch quickly, and wear poorly if switched often. But, here's how I get around that. The SSR controls the power going through the relay module, and the mechanical relays are never switched on or off with power applied.
Here's the process for start up:
If heaters need to be added or removed, the same process is followed.
This configuration is very protective of the mechanical relays, and an argument could be made that the mechanical relays will last a very long time, much longer than usual, in this configuration. Most of the wear on mechanical relays is when arcing occurs during the make and break process when power is applied. This should not ever happen because make and break only occurs when the power is removed by the SSR.
Here you can see the black wires coming off the T1 terminal of the SSR, going down to the terminal block and feeding the common terminals of the mechanical relays. White wires come out of the mechanical relays and connect to the heaters. Each heater is under 5a which is half of the 10a rating of each individual mechanical relay, if you believe the mechanical relay ratings. Running all four heaters is under 20a, and my Carlo Gavazzi relay is rated for 25a with a heat sink, and I absolutely believe the ratings on the SSR. You can't really see the heat sink under the SSR, but it's a large one.
I used the terminal block because I don't really trust the screw down blocks on the relay module. I'm actually nearly certain that the wires will loosen and disconnect if the wires move at all. That's not an option when dealing with line voltage.
Some people use infrared ovens with fancy quartz heaters for their oven, and that's a great idea if available. My oven was found on a curb with a broken power switch, so I used what I had. It has only resistive heaters, which are slow to heat up and slow to cool down, not really ideal for a reflow oven.
To supplement the oven and increase the responsiveness, especially at lower temperatures, I added two halogen lamp holders (118mm R7S) and halogen bulbs, one on top and one on bottom. For my implementation, I used two 500w lamps, which brough my oven up to nearly 20amps. If you don't need as many watts, lower wattage lamps are commonly available, and cheap. Be sure to get holders that have high temperature wires, some really cheap holders look like they have regular PVC insulation.
I made a bracket by bending a piece of stainless and drilling holes to mount the bulb holder and mount the bracket to the oven. I also used 1/4" threaded inserts (rivnuts) for the wire penetrations to keep the wires away from the sharp edges where I drilled the oven. Some sort of high temperature grommet probably would have been better, but the threaded inserts are working so far. Theads were intact for all but one that I had to drill out because the wire insulation was catching. After wires were fed through, the holes were sealed with high temp silicone. You do want to keep some distance between the oven wall and the halogen bulb.
Why halogen bulbs? They are commonly available and cheap. They are also small and easily fit in my oven after fashioning some metal brakets to hold them in place. Further, the cheap halogen bulbs are estimated to give off up to 97% of the wattage off as heat by some sources. That's perfect for an oven.
Don't you have a problem with turning the lights on and off continually? Doesn't this shorten the lifespan? Maybe it does, but again, halogen bulbs are cheap, so I'm not worried. An argument could be made that running them in an enclosed space would keep the heat on the bulb more steady, and help prolong the life of the bulb.
These bulbs worked so well, for my low temperature profiles, I use only the lower halogen bulb, all other heaters and bulbs stay off.
First, let it be known, that I'm a software guy who dabbles in hardware. I'm not an EE and in fact, I have never even taken a high school physics course. You should suspect anything I say and verify with someone who does have better knowledge of hardware and electricity.
Some of the things I have done with this build involve disabling normal safety checks. You should be very wary of these things and take approprate measures, including but not limited to the following, if you choose to implement them.
We are talking about using mains voltage (120v or 240v depending on your location), computer control and a lot of heat. Precautions must be taken, especially if you plan on running an oven unattended. Please, do not construct an oven if you are not comfortable with these issues.
Any home made device like this should incorporate hardware interlocks or fail-safes. In this case, a basic fail safe can be made for under $20. With two pieces of hardware and some wire.
Buy a contactor (20-30amps or larger depending on the amperage draw of your oven) with a line voltage coil (120v or 220v) and a thermal fuse. The thermal fuse does not need to be high amperage, 1a-5a would be perfectly fine, as the fuse will only be inline with the coil on the contactor. Make sure the voltage rating on the thermal fuse is appropriate to your situation.
Commercial grade contactors with 120v coils, as of the time of this writing, are available on ebay for under $15 shipped.
Attach hot to the contactor power lugs. Also attach a thermally insulated wire to hot, attach the thermal fuse (do not solder, crimps only) to the wire, and locate the thermal fuse somewhere on the oven or in the oven case. The other side of the thermal fuse should go back to the coil on the contactor, and the other side of the coil goes to neutral.
All this does is turn on the coil when power is applied. If the thermal fuse breaks, power to the coil is removed and the contactor opens, removing power to the oven and all electronics. It's very simple and can help reduce the chance of, though not completely eliminate, damage and destruction if things go wrong.
I am specifically not recommending a location for the thermal fuse or a rating in Celcius. How you construct your oven and what you use if for will influence proper thermal fuse sizing and location.
I have not done this yet, but it's on my short list of things to do.
It should also go without saying, but the oven should be well grounded and a plug with a ground pin should be used. Don't cut corners on safety.