HotBox - a tiny enclosure heater
100mm wide, 52mm deep and 177mm high
Warning
HotBox uses a resistive heating element inside an enclosed space. Built and configured incorrectly, it is a genuine fire risk.
This project is shared as-is for experienced makers who understand the risks. The author accepts no responsibility for damage, injury, or loss of any kind. You build and run this entirely at your own risk.
If you're not comfortable working with high-current DC systems and thermal management, please don't attempt this build.
HotBox.v18.mp4
| # | Image | Part | Description | Qty | Notes | Link |
|---|---|---|---|---|---|---|
| 1 | PTC Heater Element | 24V 200W ceramic PTC heater | 1 | Self-limiting; derate above 87°C | amazon.co.uk | |
| 2 | Square Thermal Fuse | 125°C Ceramic Thermal 250V 15A | 1 | CQC rated; soldered inline | amazon.co.uk | |
| 3 | Axial Fan | 24V 4020 axial fan | 2 | Must run at 100% when element is powered | amazon.co.uk | |
| 4 |
|
Thermistor | NTC 100K | 1 | Seats in dedicated hole in PTC body; use thermal glue | amazon.co.uk |
| 5 |
|
Thermal Glue | EC360 2W/mK thermal adhesive | 1 | Bonds thermistor to PTC element body | amazon.co.uk |
| 6 | M3×10mm SHCS | Socket head cap screw | 2 | Secures PTC heater element | ||
| 7 | M3×25mm SHCS | Socket head cap screw | 2 | Heater case to fan case | ||
| 8 | M3×30mm SHCS | Socket head cap screw | 4 | Fans through to fan duct | ||
| 9 | Heat-Set Insert | M3×4mm brass heat-set insert | 8 | amazon.co.uk | ||
| 10 | Magnets | 3×3mm N42 neodymium magnets | 8 | For lid retention | amazon.co.uk | |
| 11 | WAGO 221-412 | 2-conductor lever-nut splicing connector | 3 | Tool-free wire connections for heater, fans and thermistor | amazon.co.uk | |
| 12 | Lid | Printed part | 1 | Print in ABS/ASA | STL | |
| 13 | Heater Case | Printed part | 1 | Print in ABS/ASA | STL | |
| 14 | Fan Case | Printed part | 1 | Print in ABS/ASA | STL | |
| 15 | Fan Duct | Printed part | 1 | Print in ABS/ASA | STL | |
| 16 | Fan Duct ZeroG Nebula | Printed part | 1 | Print in ABS/ASA; alternative duct for ZeroG Nebula enclosure | STL |
I made this for me on my ZeroG Nebula printer. I've provided a fan duct without the filler plate which will bolt onto a 2020 extrusion, but I'm not going to try and build mounting solutions for other printers without being able to test or measure.
If you want to use this heater on your printer, either:
- Modify the fan duct, test it, let me know and it can be added to the repo
- spec out what you need and I'll build it, (untested) and publish it
(Pictures to follow soon)
- Fit all heat-set inserts into the correct holes with a soldering iron
- Trim the ends of the thermal fuse so that the wires fit neatly into the wagos without touching the lid
- Cut one side of the PTC thermal heater wire to length so that it fits in the wag opposite the cable hole without pushing the lid
- Fill the round hole in the PTC heater with thermal glue and insert the thermistor and wait to DRY (this will not cure until up to temperature later)
- Slot the WAGO's and thermal fuse into the correct holes
- Feed the wires through the cable hole. This will be SUPER TIGHT! The hole is as large as it can be whilst maintaining the small size of the heater
- Before pulling all the wires completely through, put the wired end of the PTC heater under the thermal fuse holder and then slot in the front. Wiggle it a bit and get it into position
- Screw down the PTC heater
- Pull the wires through so that they are neat and not obstructing the heater
- Push the wires through the fan case and down through the middle hole, but do not tighten
- Put the fans in the right position in the fan case - airflow going up into the PTC element
- Feed the fan wires through the middle hole
- Feed everything through the middle hole of the fan duct (you may need to join the PTC wire to a new piece of wire and shield it)
- Screw the fan case to the fan duct
- Keeping the wires tight with one hand push it all together
- Screw the heater case to the fan case
- Use a drop of superglue in the magnet holes and tap/push/force them in with something hard
This is very much dependent on your setup, but I did the following:
- Used WAGO's on the printer body to connect everything together to keep it neat
- Used a Mosfet running off the PSU and a fan PIN to modulate it
- Connected the thermistor and fans to the main board
- Configured everything in Klipper (see below)
# ============================================================
# HOTBOX Config
# ============================================================
[heater_generic chamber]
heater_pin: PA3 # <--------------- change this
sensor_type: Generic 3950
sensor_pin: PF6 # <--------------- change this
min_temp: 0
max_temp: 70
control: pid
pwm_cycle_time: 0.01
pid_Kp: 100
pid_Ki: 0.3
pid_Kd: 30
[verify_heater chamber]
max_error: 600
check_gain_time: 300
hysteresis: 5
heating_gain: 0.5
# Fans will run at 100% any time the hotbox element exceeds 35, regardless
# of what Klipper is doing (PID calibration, printing, idle, anything).
[temperature_fan hotbox]
pin: PD14 # <--------------- change this
sensor_type: Generic 3950
sensor_pin: PF7 # <--------------- change this
min_temp: 0
max_temp: 120
target_temp: 35.0
min_speed: 0.0
max_speed: 1.0
control: watermark
max_delta: 1
kick_start_time: 0.5
# ============================================================
# HOTBOX CONTROL LOOP
# Runs every 5 seconds from printer startup
# Stores chamber target into HOTBOX_STATE so backoff can restart
# Fan speed is now managed automatically by [temperature_fan hotbox]
# This loop only handles: state tracking + overtemp backoff
# ============================================================
[delayed_gcode HOTBOX_CONTROL]
initial_duration: 5
gcode:
{% set hotbox_temp = printer['temperature_fan hotbox'].temperature %}
{% set chamber_target = printer['heater_generic chamber'].target %}
{% set heater_on = chamber_target > 0 %}
{% if heater_on %}
SET_GCODE_VARIABLE MACRO=HOTBOX_STATE VARIABLE=target VALUE={chamber_target}
{% endif %}
{% if hotbox_temp > 87 and heater_on %}
SET_HEATER_TEMPERATURE HEATER=chamber TARGET=0
UPDATE_DELAYED_GCODE ID=HOTBOX_BACKOFF DURATION=60
RESPOND MSG="HotBox: Element at {hotbox_temp}C - 60s backoff, will restart automatically"
{% endif %}
UPDATE_DELAYED_GCODE ID=HOTBOX_CONTROL DURATION=5
# ============================================================
# HOTBOX BACKOFF TIMER
# Triggered by HOTBOX_CONTROL after element overtemp
# Calls HOTBOX_RESTART after 60 seconds
# ============================================================
[delayed_gcode HOTBOX_BACKOFF]
initial_duration: 0
gcode:
HOTBOX_RESTART
[include hotbox_macros.cfg] # <--------------- put this with your other includes or at the end of the file
# ============================================================
# hotbox_macros.cfg
# HotBox enclosure heater user macros
# Include this file from printer.cfg with [include hotbox_macros.cfg]
# ============================================================
# ============================================================
# HOTBOX_STATE
# Stores the intended chamber target so backoff can restart
# This is set both by HOTBOX_ON and by the HOTBOX_CONTROL loop
# so it works whether you use the macro or set temp directly in Mainsail
# ============================================================
[gcode_macro HOTBOX_STATE]
variable_target: 0
gcode:
# ============================================================
# HOTBOX_ON
# Usage: HOTBOX_ON TARGET=60
# Heats enclosure to target temperature (default 60C, max 70C)
# Fans start automatically via [temperature_fan hotbox] - no manual control needed
# ============================================================
[gcode_macro HOTBOX_ON]
description: Heat enclosure to target temperature. Usage: HOTBOX_ON TARGET=60
gcode:
{% set TARGET = params.TARGET|default(60)|float %}
{% if TARGET > 70 %}
RESPOND MSG="HotBox: Target {TARGET}C exceeds safe maximum of 70C - aborting"
{% elif TARGET < 30 %}
RESPOND MSG="HotBox: Target {TARGET}C is too low - minimum is 30C"
{% else %}
SET_GCODE_VARIABLE MACRO=HOTBOX_STATE VARIABLE=target VALUE={TARGET}
SET_HEATER_TEMPERATURE HEATER=chamber TARGET={TARGET}
UPDATE_DELAYED_GCODE ID=HOTBOX_CONTROL DURATION=5
RESPOND MSG="HotBox: Heating enclosure to {TARGET}C - fans will run automatically"
{% endif %}
# ============================================================
# HOTBOX_OFF
# Turns heater off and clears stored target
# Fans continue automatically until hotbox element cools below 40C
# ============================================================
[gcode_macro HOTBOX_OFF]
description: Turn off HotBox heater. Fans run automatically until hotbox cools below 40C
gcode:
SET_GCODE_VARIABLE MACRO=HOTBOX_STATE VARIABLE=target VALUE=0
SET_HEATER_TEMPERATURE HEATER=chamber TARGET=0
RESPOND MSG="HotBox: Heater off - fans will run automatically until hotbox cools below 40C"
# ============================================================
# HOTBOX_STATUS
# Prints current hotbox status to console
# ============================================================
[gcode_macro HOTBOX_STATUS]
description: Report current HotBox temperatures and fan status
gcode:
{% set hotbox_temp = printer['temperature_fan hotbox'].temperature %}
{% set chamber_temp = printer['heater_generic chamber'].temperature %}
{% set chamber_target = printer['heater_generic chamber'].target %}
{% set fan_speed = printer['temperature_fan hotbox'].speed %}
{% set intended_target = printer['gcode_macro HOTBOX_STATE'].target %}
RESPOND MSG="HotBox Status:"
RESPOND MSG=" Chamber temp: {chamber_temp}C / Target: {chamber_target}C"
RESPOND MSG=" Hotbox element: {hotbox_temp}C"
RESPOND MSG=" Fan speed: {(fan_speed * 100)|round}%"
RESPOND MSG=" Stored target: {intended_target}C"
# ============================================================
# HOTBOX_RESTART
# Called automatically after 60s backoff - do not call manually
# Restarts heater using stored target if element has cooled below 75C
# ============================================================
[gcode_macro HOTBOX_RESTART]
description: Internal - restarts heater after backoff cooldown
gcode:
{% set intended_target = printer['gcode_macro HOTBOX_STATE'].target %}
{% set hotbox_temp = printer['temperature_fan hotbox'].temperature %}
{% if intended_target > 0 %}
{% if hotbox_temp < 70 %}
SET_HEATER_TEMPERATURE HEATER=chamber TARGET={intended_target}
RESPOND MSG="HotBox: Element cooled to {hotbox_temp}C - restarting heater to {intended_target}C"
{% else %}
RESPOND MSG="HotBox: Element still at {hotbox_temp}C - waiting another 60s"
UPDATE_DELAYED_GCODE ID=HOTBOX_BACKOFF DURATION=60
{% endif %}
{% else %}
RESPOND MSG="HotBox: No stored target - not restarting"
{% endif %}-
PID calibration may not work close to the enclosure's thermal limit. If the enclosure cannot reliably reach and re-reach the target temperature through multiple oscillation cycles, the calibration will fail. Try a lower target temperature (e.g. TARGET=50 instead of TARGET=60).
-
PTC heater elements are self-limiting — they reduce power output as they approach their rated temperature. This makes PID calibration difficult as the element may throttle itself during the calibration oscillation cycles.
-
Do NOT run PID_CALIBRATE with the fans configured as [fan_generic] controlled via delayed_gcode macros. Klipper holds the gcode queue during PID_CALIBRATE, which prevents delayed_gcode from executing, meaning fans will not run. This WILL cause the element to overheat. The fans MUST be configured as [temperature_fan] so they operate independently of the gcode queue.
-
The hotbox fans must run at 100% whenever the PTC element is receiving power. This is not optional — the element will overheat and blow the thermal fuse without active cooling.
-
Always let the fans cool the element down to 50 degrees or below before switching the machine off.
-
There is a big thermal delay between the PTC element and the thermistor due to the distance between the two. Use thermal glue to bridge the gap, but factor this delay into any calculations.
-
Running the PTC heater above 87°C for extended periods risks triggering the 125°C thermal fuse. The HOTBOX_CONTROL overtemp cutoff should be set to 87°C maximum.
-
The thermistor must be seated directly in the dedicated hole in the PTC element body. Poor contact will cause it to read significantly below actual element temperature, meaning the emergency stop and overtemp cutoff will not trigger in time to protect the thermal fuse.
-
The [temperature_fan] target is user-editable in Mainsail/Fluidd. Setting it above the element's safe operating temperature will prevent fans from running at the correct threshold. Do not modify this value.
-
Klipper's max_temp on the [temperature_fan] section is an emergency shutdown threshold, not a cap on the settable target. Setting it too low will cause Klipper to hard-stop during normal operation.
-
There needs to be enough overhead in the PSU to power a 200W PTC heater on the 24v rail, do the maths and ensure that there's enough power. If not, run an additional 200W DIN rail PSU or upgrade
-
Do NOT use
control: watermarkfor the chamber heater. Bang-bang switching of the heating element causes hard current transients on the shared 24V rail which affect TMC stepper driver behaviour, producing Z-axis banding artefacts on prints. Always usecontrol: pidwithpwm_cycle_time: 0.01to smooth current draw to a near-constant average.
- BentoBox - I LOVE this and it was what triggered the idea for this design. Simple but effective in small spaces
- BentoBox Fan Duct for ZeroG - This is an absolutely great idea and I used the theory to build a similar integration for HotBox