3D printed 10" homelab rack with ESP32 fan control for Strix Halo cooling

August 4, 2026 - Rohan van der Walt

Problem

Over time my office accumulated lots of devices just standing around, a NAS, a network switch, a "home server" and some rpis sitting around connected with a mess of cables. I came across the "labrax" project to put all of this into one neat case.

The heat coming from the EVO-X2 used for local inference was also a big problem, it's an AMD Ryzen AI Max+ 395 "Strix Halo" with 128GB of unified LPDDR5X, running llama-swap in front of llama.cpp so I can hot-swap models. Under sustained inference it sat at 98C, probably getting throttled.

The finished 3D printed Lab Rax 10 inch rack with the GMKtec EVO-X2 and NAS mounted
The finished stack. Top fan panel, EVO-X2 (stripped), fan shelf, NAS.

The rack: Lab Rax

I printed the Lab Rax, a modular 10" rack system by Michael Klements. There is an original version using brass heat-set inserts and a bolted version needing only M6 button heads and nuts. I went with the bolted version, so it comes apart cleanly when I re-shelf something. The full collection has shelves, blanking plates and fan holders, and The DIY Life has a good write-up.

Check your filament before you start. PLA is all I had initially, but PLA creeps and softens well below the temperatures this rack was expected to experience. That gave me a good reason to upgrade my 3D printer nozzle to hardened steel so that I can print PETG.

Printed on a Bambu P1S. (256mm bed limit)

Custom parts, designed by an LLM

The frame is downloaded, but my components are not standard rack gear. Three custom parts:

  1. Top fan panel. The bundled Lab Rax panel is drilled for a 120mm fan; I wanted a 140mm Noctua. Same 228 x 171.5 x 3mm footprint, one round 133mm cutout (140mm frame leaves a ~3.5mm seating lip), four 4.4mm holes on the standard 124.5mm square.
  2. 1U fan mount. Open-frame bracket holding a 140mm fan flat, so it moves air vertically through the rack. Ears bolt to the front rails, wings carry the fan centred in the case depth (110mm into 220mm).
  3. Enclosure for the electronics - vented screw-down box with M3 corner bosses.

I was impressed by how easy it was to get an LLM to generate CAD parts.

What made it work:

  • Give it real measurements. use a caliper, take photos, draw simple diagrams, this all helped.
  • Use the correct vocabulary. Collar, boss, gusset, fillet, pilot hole, seating lip.
  • Always open the result and measure it before printing. Open your obj and use the measuring tool to check critical dimensions (it does make mistakes).

I had to make some hand edits afterwards, but it got me to the end result much faster.

The custom printed 1U fan mount holding a 140mm Noctua fan flat in the rack
The printed 1U fan mount, holding the lower Noctua flat under the EVO-X2.

Stripping the EVO-X2

The EVO-X2 came out of its case entirely. Inside a closed rack it does not need a second shell, and that shell was what trapped the heat.

The GMKtec EVO-X2 mounted in the rack with its outer case removed, internal fan and board visible
EVO-X2 with the body off. Front IO still reachable through the printed faceplate.

The fans

Two Noctua NF-A14x25 G2 PWM chromax.black 140mm fans - one in the top panel, one on the shelf below the EVO-X2.

These are superb. PWM controllable from 1500rpm all the way down, and at low speed they are completely silent.

The problem, I had no way to control the 4-pin fans. Another mini-project.

The controller

I used an ESP32 to read the rack air temperature from a DS18B20 (mounted right behind the EVO-X2, and that drives the fan PWM control.

A DALLAS DS18B20 temperature sensor on a KY-001 breakout board
DS18B20 on a KY-001 breakout. 1-Wire, 4.7k pull-up already on the board.

Design decisions

  • Continuous proportional curve, not on/off.
  • No power switching. The 12V line stays permanently on and only the PWM signal is modulated, at 0% duty the fans stop.
  • One 12V brick. Feeds the fans directly and a buck steps it to 5V for the ESP32.
duty
100% |                          _________
     |                 ________/
     |         _______/
 ~0% |________/
     +-------|--------|--------|--------
            30C      40C      50C

Firmware

The core function simply converts temperature to "fan duty" (8-bit output).

// fan_curve.h - pure temperature -> PWM-duty mapping.
static const float FAN_TEMP_MIN_C = 30.0f;  // at/below this: fans idle (0% PWM)
static const float FAN_TEMP_MAX_C = 50.0f;  // at/above this: 100% PWM

// Map a temperature (deg C) to an 8-bit PWM duty (0..255).
// Invalid readings -> 255 (fail-safe full speed).
static inline uint8_t tempToDuty(float tempC) {
  if (isnan(tempC) || tempC < -40.0f || tempC > 125.0f) {
    return 255;  // fail-safe: sensor fault / disconnected (DS18B20 = -127)
  }
  if (tempC <= FAN_TEMP_MIN_C) return 0;
  if (tempC >= FAN_TEMP_MAX_C) return 255;
  float frac = (tempC - FAN_TEMP_MIN_C) / (FAN_TEMP_MAX_C - FAN_TEMP_MIN_C);
  long duty = lround(frac * 255.0f);
  if (duty < 0) duty = 0;
  if (duty > 255) duty = 255;
  return (uint8_t)duty;
}

Any invalid reading returns 255. setup() also writes 255 before the first read. So if anything goes wrong, fans go to 100%.

Testing

Simulation. Wokwi has an ESP32 and a DS18B20 slider but no 4-pin fan. It was fine for simple simulations, but getting a slot to compile on the free tier was flaky.

Benchtop. On a current-limited benchtop PSU at ~0.8A.

Bench test setup with the ESP32 protoboard, LM2596 buck converter and a fanHolding the DS18B20 sensor to force the fans to spin up during bench testing
Bench bring-up. LM2596 display reading 5.0 is the ESP32 supply. Right: warming the sensor by hand to force the fan speed.

Holding the sensor between my fingers was enough to activate the >30C fan level.

The permanent build

Closeup of the soldered protoboard with a socketed ESP32-WROOM-32D module
The soldered board. ESP32 on female headers, not soldered down.

This was my first real soldering project.

The lower Noctua fan mounted in the rack with the printed controller enclosure beside it
Installed. Fan on the printed shelf, controller box on the left, DS18B20 up at the EVO-X2 exhaust.

The DS18B20 sits at the exhaust side of the EVO-X2, so it should engage whenever inference is happening.

Results

The Lab Rax works great to tidy up my office, and it looks interesting. But I'll probably go for a bigger rack next time.

The EVO-X2 temprature is now down to around 88C under sustained inference.

The Noctua fans are unbelievably silent, I'm really impressed. Below 30C the fans are stopped, and even once they are running at low duty I cannot hear them. They only ramp up while a model is generating.

Parts

PartRole
ESP32-WROOM-32DLEDC 25kHz PWM + 1-Wire
DS18B20 (KY-001 module)Rack air temperature, onboard 4.7k pull-up
2x Noctua NF-A14x25 G2 PWM chromax.black140mm, ~0.14A @ 12V each
LM2596 buck converter12V to 5V for the ESP32
12V/2A barrel PSU, 5.5x2.1mm centre-positiveSingle supply for fans + buck
5.5x2.1mm female barrel jack to screw terminal12V entry point, no soldering
5x7cm double-sided protoboardThe permanent build
M6 button head screws + nutsLab Rax bolted version
PETG filament + hardened steel nozzleRack, mounts, enclosure

Keywords

#Homelab #10InchRack #LabRax #3DPrinting #PETG #ESP32 #DS18B20 #Noctua #PWM #StrixHalo #GMKtec #LocalLLM #llamacpp #CadQuery #Fusion360 #BambuLab

×