When I started with 3D printing, I learned very fast that moisture in the filament can ruin your prints. Many 3D printing filaments absorb moisture from the air, so a dedicated dryer box is a great tool to have. I decided to use a simple four-liter cereal container as my starting point to make one and design all the electronics and software myself around it.
This project was a huge learning experience for me. Even though my first attempt didn't work out exactly as I had hoped, it taught me so much about the entire process, from planning and design to assembly and testing. I also discovered a surprising problem with a component that I never knew existed.
I'm going to walk you through my entire build process, from the circuit design to the 3D-printed parts. I'll also show you the unexpected issue that popped up during testing. My goal is to share what I learned so we can all make an even better version two together.
Designing the Electronics
For the heart of the system, I chose a 12V 50W PTC heater because it's powerful and gets warm safely. The brains of the operation is a NodeMCU, which is a board based on the ESP8266 microcontroller. I originally thought about using its Wi-Fi feature, but I decided to keep things simple for this first version and just use it for local control. To know what's happening inside the box, I used a BME680 sensor. It measures both temperature and humidity, and I found it to be much more stable than other sensors I tried.
I wanted to see the readings and adjust the temperature easily, so I added a mini OLED screen and a rotary encoder. The screen shows the current temperature, the target temperature, and a bar that shows how much power the heater is getting. The rotary knob lets me change the target temperature and turn the whole system on or off with a press. I programmed it all to work together, with a little logo animation that plays when you first power it on.
The biggest challenge was handling the power. The heater can draw over four amps, which is way too much for the microcontroller to handle directly. I used a MOSFET, which is like a heavy-duty switch, to control the heater's power. But because the NodeMCU only outputs 3.3 volts, I needed an extra transistor to help me flip that powerful switch on and off completely. I also added an LED that lights up whenever the heater is on, which gives me a quick visual indicator of what's happening.
Arduino Code
Below is the full Arduino code that I used on the filament dryer. Feel free to use it on your projects and modify it as you need.
// Filament Dryer Project by Taste The Code
// - 128x32 SSD1306 OLED
// - HW-040 rotary encoder (CLK, DT, SW)
// - Heater MOSFET control
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Zanshin_BME680.h>
#include <Encoder.h>
#include <PID_v1.h>
// ----------------- CONFIG -----------------
#define HEATER_PIN D8
#define ENCODER_CLK D6
#define ENCODER_DT D7
#define ENCODER_SW D5
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
BME680_Class BME680;
Encoder myEnc(ENCODER_CLK, ENCODER_DT);
// ----------------- VARIABLES -----------------
double setpoint = 50.0; // default setpoint (°C)
bool systemOn = false; // system OFF at power-up
long oldPosition = -999;
// PID variables
double input; // Current temperature for PID
double output; // PID output (0-255 for analogWrite)
double Kp = 50.0; // Proportional gain
double Ki = 0.1; // Integral gain
double Kd = 10.0; // Derivative gain
// PID controller instance
PID myPID(&input, &output, &setpoint, Kp, Ki, Kd, DIRECT);
unsigned long lastRead = 0;
float temperature = NAN;
float humidity = NAN;
float pressure_hpa = NAN;
float gas_res = NAN;
bool bmeAvailable = false;
bool firstBmeReadingIgnored = false;
// Overlay control
unsigned long lastOverlay = 0;
bool showOverlay = false;
String overlayText = "";
//Logo
const unsigned char PROGMEM screen [] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x1f, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xff, 0xff, 0xfe, 0x07, 0xff, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x03, 0xe0, 0x00, 0x1f, 0x8f, 0xff, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x07, 0x00, 0x40, 0x81, 0xcf, 0xff, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0e, 0x30, 0x04, 0x30, 0xc0, 0xf0, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0f, 0x00, 0x20, 0x01, 0xc0, 0xf0, 0x3f, 0xc0, 0xfe, 0x7f, 0x83, 0xf0, 0x0a, 0x00, 0x00, 0x00,
0x0f, 0xe0, 0x00, 0x1f, 0xc0, 0xf0, 0x3f, 0xe1, 0xff, 0x7f, 0x87, 0xfc, 0x0b, 0xb0, 0x00, 0x00,
0x07, 0xff, 0xff, 0xff, 0xc0, 0xf0, 0x3d, 0xf3, 0xcf, 0x7f, 0x8f, 0x3c, 0x0a, 0xf0, 0x00, 0x00,
0x07, 0xff, 0xff, 0xff, 0xc0, 0xf0, 0x00, 0x73, 0x80, 0x1c, 0x1e, 0x1e, 0x0e, 0xe0, 0x00, 0x00,
0x07, 0xff, 0xff, 0xff, 0x80, 0xf0, 0x00, 0x73, 0xc0, 0x1c, 0x1c, 0x0e, 0x00, 0x00, 0x04, 0x00,
0x03, 0xff, 0xff, 0xff, 0x00, 0xf0, 0x0f, 0xf3, 0xfc, 0x1c, 0x3f, 0xfe, 0x00, 0x00, 0x04, 0x00,
0x01, 0xff, 0xff, 0xff, 0x00, 0xf0, 0x3f, 0xf1, 0xfe, 0x1c, 0x3f, 0xfe, 0x00, 0x00, 0x0c, 0x00,
0x00, 0xff, 0xff, 0xfe, 0x00, 0xf0, 0x78, 0x70, 0xff, 0x1c, 0x3f, 0xf8, 0x07, 0xbe, 0x7c, 0xf0,
0x00, 0x7f, 0xff, 0xfc, 0x00, 0xf0, 0x70, 0x70, 0x07, 0x1c, 0x1c, 0x00, 0x0c, 0x66, 0x4d, 0x98,
0x00, 0x3f, 0xff, 0xf0, 0x00, 0xf0, 0x70, 0x72, 0x07, 0x1c, 0x1e, 0x00, 0x18, 0x62, 0xc5, 0xf8,
0x00, 0x0f, 0xff, 0xc0, 0x00, 0xf0, 0x78, 0xf3, 0x8f, 0x1f, 0xcf, 0xfc, 0x18, 0x62, 0xc5, 0x80,
0x00, 0x1f, 0xff, 0xe0, 0x00, 0xf0, 0x3f, 0xff, 0xff, 0x1f, 0xc7, 0xfc, 0x0c, 0x66, 0x6d, 0x80,
0x00, 0x0f, 0xff, 0xe0, 0x00, 0xf0, 0x1f, 0xf3, 0xfc, 0x0f, 0xc3, 0xfc, 0x07, 0xbc, 0x7c, 0xf0,
0x00, 0x01, 0xfe, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// ----------------- SETUP -----------------
void setup() {
pinMode(HEATER_PIN, OUTPUT);
pinMode(ENCODER_SW, INPUT_PULLUP);
digitalWrite(HEATER_PIN, HIGH);
Serial.begin(115200);
delay(200); // allow serial to initialize
// OLED init - do NOT force Wire pins here (NodeMCU default SDA=D2, SCL=D1)
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 not found"));
while (1);
}
// Force display ON and max contrast
display.ssd1306_command(SSD1306_DISPLAYON);
display.ssd1306_command(SSD1306_SETCONTRAST);
display.ssd1306_command(255); // max brightness
display.clearDisplay();
display.display();
// Clear the buffer
display.clearDisplay();
display.drawBitmap(0, 0, screen, 128, 32, 1);
display.display();
delay(2000);
// BME680 init
Serial.println(F("Initializing BME680..."));
// Try to start BME680 using I2C standard mode (library constant)
if (!BME680.begin(I2C_STANDARD_MODE)) {
Serial.println(F("BME680 not found - readings will be N/A"));
bmeAvailable = false;
} else {
bmeAvailable = true;
// Configure the sensor: oversampling + IIR + gas heater (same as example)
BME680.setOversampling(TemperatureSensor, Oversample16);
BME680.setOversampling(HumiditySensor, Oversample16);
BME680.setOversampling(PressureSensor, Oversample16);
BME680.setIIRFilter(IIR4);
// Gas heater: 320°C for 150 ms (example)
BME680.setGas(320, 150);
Serial.println(F("BME680 configured"));
}
// Initialize PID controller
myPID.SetMode(AUTOMATIC); // Turn the PID on
myPID.SetOutputLimits(0, 255); // Output range for analogWrite (8-bit PWM)
myPID.SetSampleTime(1000); // Calculate new output every 1000ms (1 second)
Serial.println(F("PID controller initialized"));
Serial.println("System started");
}
// ----------------- Helper: read BME680 (non-blocking as in the example) ---
void readBME680() {
// Only attempt if library says sensor present
if (!bmeAvailable) return;
// Raw values returned by Zanduino functions
static int32_t temp_raw = 0;
static int32_t humid_raw = 0;
static int32_t press_raw = 0;
static int32_t gas_raw = 0;
// getSensorData fills the int32_t references (see example)
BME680.getSensorData(temp_raw, humid_raw, press_raw, gas_raw);
// The library returns integer units:
// - temperature in centi-degrees (e.g. 2731 -> 27.31 °C) -> divide by 100.0
// - humidity in milli-percent (e.g. 52345 -> 52.345 %) -> divide by 1000.0
// - pressure in Pascals -> divide by 100.0 to get hPa
// - gas is returned in scaled units (example prints gas/100.0)
temperature = (float)temp_raw / 100.0f;
humidity = (float)humid_raw / 1000.0f;
pressure_hpa= (float)press_raw / 100.0f;
gas_res = (float)gas_raw / 100.0f;
if (!firstBmeReadingIgnored) {
firstBmeReadingIgnored = true;
Serial.println(F("First BME680 reading ignored (per example)"));
return;
}
Serial.printf("BME -> Temp: %.2f C Hum: %.3f %% Press: %.2f hPa Gas: %.2f\n",
temperature, humidity, pressure_hpa, gas_res);
}
// ----------------- LOOP -----------------
void loop() {
// --- Encoder rotation ---
long newPosition = myEnc.read() / 2;
if (newPosition != oldPosition) {
oldPosition = newPosition;
setpoint = constrain(50.0 - newPosition, 10.0, 80.0);
// Trigger overlay for setpoint
overlayText = String((int)setpoint) + "C";
showOverlay = true;
lastOverlay = millis();
}
// --- Encoder button toggle (push) ---
if (digitalRead(ENCODER_SW) == LOW) {
delay(50);
if (digitalRead(ENCODER_SW) == LOW) {
systemOn = !systemOn;
// Overlay ON/OFF message
overlayText = systemOn ? "ON" : "OFF";
showOverlay = true;
lastOverlay = millis();
while (digitalRead(ENCODER_SW) == LOW); // wait for release
delay(50);
}
}
// --- Read BME680 every 2s ---
if (millis() - lastRead > 2000) {
lastRead = millis();
if (bmeAvailable) {
readBME680(); // sets global temperature/humidity/pressure/gas_res
} else {
// nothing to do; readings stay NAN
}
}
// --- PID Heater control ---
if (systemOn && !isnan(temperature)) {
input = temperature; // Feed the current temperature to the PID as input
myPID.Compute(); // Perform the PID calculation. Result goes to 'output'
// Use the PID output to control the heater with PWM
// Invert output since heater circuit is active LOW (255 = OFF, 0 = FULL ON)
analogWrite(HEATER_PIN, 255 - output);
} else {
// System is off or sensor error - turn heater off completely
analogWrite(HEATER_PIN, 255); // 255 = 100% OFF for active LOW circuit
}
// --- OLED update ---
display.clearDisplay();
if (showOverlay) {
// Center overlay text in size 2
display.setTextSize(2);
display.setTextColor(SSD1306_WHITE);
int x = (SCREEN_WIDTH - (6 * overlayText.length() * 2)) / 2;
if (x < 0) x = 0;
display.setCursor(x, 8); // centered vertically in 32px
display.print(overlayText);
if (millis() - lastOverlay > 2000) {
showOverlay = false;
}
} else {
display.setTextColor(SSD1306_WHITE);
// Current temperature (big, left)
display.setTextSize(2);
display.setCursor(5, 0);
if (!isnan(temperature)) {
char buf[12];
snprintf(buf, sizeof(buf), "%.1f", temperature);
display.print(buf);
} else {
display.print("--.-C");
}
// Humidity (big, top right)
display.setTextSize(2);
display.setCursor(80, 0);
if (!isnan(humidity)) {
char hbuf[8];
snprintf(hbuf, sizeof(hbuf), "%d%%", (int)round(humidity));
display.print(hbuf);
} else {
display.print("--%");
}
// Setpoint (big, bottom left)
display.setTextSize(2);
display.setCursor(5, 16);
display.printf("T:%2.0f", setpoint);
// Status (big, bottom right)
display.setTextSize(2);
display.setCursor(80, 16);
if (!systemOn) {
display.print("OFF");
} else {
// Simple indicator: show if heater is actively heating or idle
// Check if PID output indicates heating is needed (output > threshold)
if (output > 20) { // If PID says we need heating (output 0-255, >20 means heating)
display.print("HEAT");
} else {
display.print("IDLE");
}
}
// Add small heater power level bar at bottom of screen (1px high)
if (systemOn) {
// Calculate current heater power level for the bar
// 'output' from PID is 0-255, where 255 = full heating needed
int barWidth = (int)(output / 255.0 * (SCREEN_WIDTH - 2)); // Leave 1px margin on each side
// Draw the 1-pixel high bar at the very bottom
if (barWidth > 0) {
display.fillRect(1, 31, barWidth, 1, SSD1306_WHITE);
}
}
}
display.display();
}
Building the Enclosure
My main enclosure is a simple four-liter plastic cereal container. I chose it because it's airtight with a good seal on the lid, and it's just the right size to fit a standard one-kilogram spool of filament inside. Since there is not enough space inside to house everything in the bottom, my plan was to mount everything on the lid itself to keep the main box clear for the filament roll.
Since heat rises, I knew I needed a fan to force the hot air down and circulate it around the filament. I designed a special adapter in Fusion that holds the heater and allows me to attach a 40mm fan to it. This little duct directs all the airflow from the fan through the heater and down into the box.
For the user controls, I designed and printed a front panel that holds the rotary encoder and has a slot for the OLED screen. I also printed a flat, foldable spool holder that fits perfectly inside the box to keep the filament centered. Fitting everything inside was a real challenge so for any future upgrades, I will have to modify the box to be a bit larger so I can more easily fit everything inside.
You can download the 3D models I created here, the mini oled screen cover here and the spool holder here.
Wiring and Assembly
This is where the project got a bit messy. I started by soldering wires directly to all the components. I replaced the pins on the rotary encoder with wires so I could route them through the case, and I soldered wires straight to the temperature sensor. For the main circuit, I moved everything from my breadboard to a perforated prototype PCB. I carefully connected all the components by bending their legs and soldering them together underneath the board. It was a tight squeeze, and it's not the prettiest circuit, but it worked.
Next, I began the tricky process of fitting everything inside the printed parts and the cereal box. I used hot glue to secure the NodeMCU, the prototype PCB, and the step-down power converter to the underside of the lid. I mounted the rotary encoder and screen into the front panel I designed. I then screwed the heater and fan assembly onto the lid as well. The sensor was glued to the inside wall of the box to monitor the air temperature.
The final assembly was a tight fit. The springy wires wanted to push the lid up, and I had to add more hot glue in places to keep things secure. It’s not the most elegant internal layout but for a first version, it was good enough to test. After a final check to make sure all the connections were solid, it was time to see if it would actually work.
Testing the Dryer
With everything assembled, I plugged it in for the first real test. The fan immediately started spinning to circulate air, and my logo animation played on the screen. I could set my target temperature using the rotary knob and then pressed the button to turn the heater on. I could feel warm air coming out of the assembly almost right away, and the status LED lit up brightly.
I watched the temperature reading on the screen slowly climb from room temperature. The system was working, the heater was warming the air, and the fan was pushing it around the box. As the temperature got closer to my 50-degree target, I could see the power bar on the screen get smaller and the LED dim. This meant the controller was doing its job, reducing power to the heater to avoid overheating. It overshot the target a little but then settled into a stable temperature.
Finally, I put a spool of PLA filament inside, closed the lid, and started a drying cycle. The humidity reading initially dropped as the air warmed up. After a while, the humidity started to rise again, which was a good sign because it meant moisture was being pulled out of the filament and into the air. The system held a steady temperature, and everything seemed to be working perfectly... for about two hours. Then, the first major problem appeared.
Problems and Lessons Learned
After running for about two hours, I ran into a major issue: the screen suddenly went blank. The software was still working because I could see the heater LED changing brightness when I adjusted the knob, but the display was completely off. I thought it was a wiring problem at first, but after letting everything cool down overnight and powering it back on, the screen worked perfectly again. This confirmed my theory—the heat inside the box was causing the screen to fail. It's a problem I never anticipated, and it basically made the display useless during long drying cycles.
For the next version, I have a lot of ideas to improve this. For start, I need to figure out a way how to mount everything with insulation in between so the electronics can stay cool. I also want to make the box bigger so I can more easily fit everything inside and possibly design a proper custom PCB to make the wiring much cleaner and more reliable than my hand-soldered prototype board.
The biggest lesson I learned is to always consider the operating environment of every component. Just because something works on your bench doesn't mean it will work inside a hot, enclosed space. This project was a fantastic lesson in thermal management and designing for real-world conditions, not just ideal ones. Even though version one has a flaw, it proved the core idea works, and that's a great starting point for version two.
Conclusion and next steps
Even with the screen issue, I'm calling this first attempt a success. It proved that the concept works. The heater, the fan, and the controller all did their job to create a stable, hot environment that started pulling moisture from the filament. The problem with the display is a setback, but it's one I know how to fix for the next version.
This is where I need your help. I know you have great ideas, so please share them with me. Have you encountered a similar problem with a screen overheating? How would you solve it? What other features would you want to see in a DIY filament dryer? Your feedback will be incredibly valuable for designing version two.
Make sure to subscribe so you don’t miss the next video where I build the improved dryer using everything we learned. I'm excited to take all your suggestions and build a much better version. Thanks for following along, and I'll see you in the next one
Just a heads-up: the links below are affiliate links. This means if you click through and make a purchase, I may earn a small commission at no extra cost to you. This is a simple way to support my work and helps me continue creating free content for you. Thanks for your support!
Tools and Materials Used
- 4L Cereal Container - https://s.click.aliexpress.com/e/_on6WpSp
- NodeMCU ESP8266 Development Board - https://s.click.aliexpress.com/e/_omICCIl
- 12V 50W PTC Heater - https://s.click.aliexpress.com/e/_okIG0dL
- BME680 Sensor - https://s.click.aliexpress.com/e/_opY77KV
- IRFZ44N MOSFET - https://s.click.aliexpress.com/e/_oEo7FXL
- 2N2222 Transistor - https://s.click.aliexpress.com/e/_oEt2uE5
- Step Down Module - https://s.click.aliexpress.com/e/_onupeDT
- Mini OLED Screen - https://s.click.aliexpress.com/e/_onjuPqp
- Rotary Encoder - https://s.click.aliexpress.com/e/_oktmb0V
- Bench Power Supply - https://s.click.aliexpress.com/e/_olWK1tn
- Multimeter - https://s.click.aliexpress.com/e/_oBOLtKv