Skip to the content
2 layers70 × 50 mmWindow vent ctrl, the printed circuit board in 3D

Window vent ctrl

mmassico/window_air_vent

Sliding window air exchanger without heat recovery (3D models)

Size
70 × 50 mm
Copper layers
2
Thickness
1.6 mm
Parts
35
Nets
49
Pads
128
Tracks, vias
463, 17
Pours
90
Routed
Yes
KiCad
KiCad 10.0 (file 20260206)
Created
18 Oct 2026
Updated
7 Nov 2026

Overview

Window vent ctrl is an open-source printed circuit board by mmassico, from the mmassico/window_air_vent repository on GitHub, published under the CERN-OHL-P-2.0 licence. It is a 2-layer board of 70 × 50 mm with 35 parts on 49 nets, routed with 463 tracks and 17 vias.

Its parts include 10 resistors, 8 connectors, 2 ICs, 1 capacitor and 1 diode. It belongs with the ESP32 boards of this library.

README

README.md

Window vent

A demand-controlled window ventilator for one room. Two 80 mm fans move air through a foam window insert: one brings fresh air in, the other exhausts stale air. A single butterfly-damper housing closes both holes with one shaft, turned by a silent stepper motor. An ESP32 reads a CO₂ sensor and runs the fans and the damper.

There is no heat recovery: every litre of air exhausted is lost heat. In exchange it is simple, cheap and automatic. See the estimates below.

Status: prototype, running in a window. The complete unit is installed in a window, on the controller PCB and the web application, and ventilates on its own from its CO₂ reading. It closes when it's loud outside, using a microphone listening through a tube in the foam. Every printed part has been printed and test-fitted (housing, discs, motor plate, magnet cap, electronics box, tube spacer, louvers), and several were refined from those prints: the shaft holes' sacrificial fins, the box's back tabs, the louvers' snap-in combs, and the discs' seats. The lip seals are installed and tested: with the 1.6 mm neoprene, the closed discs now press evenly on the foam all round, after two rounds of tuning the discs' seats (the housing stayed as printed). The housing was then reprinted with flat shaft bosses and the discs with matching flats: the gap at the bosses is gone (the first, curved bosses left 0.3 to 0.75 mm), and the reprinted electronics box fits too, its back tabs and the notches for the fan's rubber bumpers included; the tabs make mounting it on the housing much easier. The shaft holes' sacrificial fins work as intended: no warping, and they drill out easily. The lip foam cutting guides are printed and tested: cutting the foam with them is much easier, faster and more accurate than with the paper templates. The foam-free disc_nofoam is checked in CAD but not tested yet. Its CO₂ reading agrees with a calibrated meter within the sensor's tolerance. What's still to come is time: it has only just gone in, so it hasn't seen a season yet. The geometry is also checked in CAD (closed solids, the discs swing freely, the lips stop them).

Damper housing with two 80 mm fans, stepper motor on top, spigots in the foam insert, with labels: stepper motor, motor plate, motor tower, shaft coupler, intake and exhaust fans, damper discs, shaft, spigot, damper housing, electronics box, foam insert

How it works

  • Foam insert. A block of 2 in (51 mm) XPS foam, 30 psi grade, fills the window gap (100 mm or less, so a child can't fit through). Two 83 mm holes (a 3-1/4 in hole saw, see drilling), stacked vertically: intake on top, exhaust below. An insect screen in a printed frame covers the outdoor face.
  • Damper housing. It sits on the room face of the foam, so its moving parts stay warm and dry (no ice). Its two spigots fit snugly in the foam holes; two O-rings on each are optional, for a hole that comes out loose. The housing comes out in spring, and the foam insert stays reusable.
  • Butterfly dampers. One vertical 5 mm steel shaft runs through both ducts and carries a disc in each. A quarter turn opens or closes both. Each disc closes against two half-ring lips lined with thin foam (or bare, with disc_nofoam), one on each side of the shaft. One lip is flat; the other is a 45° cone, and the half of the disc that closes against it (engraved B) has a matching 45° bevel, so neither prints as a flat overhang.
  • Motor and encoder. A NEMA 17 pancake stepper on top turns the shaft through a flexible coupler. A TMC2209 driver keeps it silent. The motor screws to a separate plate on the bench, and the plate then screws onto a frame on top of the housing from above. An AS5600 magnetic encoder at the bottom reads the absolute shaft angle, so the firmware always knows whether the dampers are really closed, with no homing and no limit switch.
  • Fans. Two 80 mm PWM fans screw onto the room end of the housing.
  • Electronics box. A tray under the housing holds a 5 × 7 cm perfboard with the ESP32 and the TMC2209 soldered on. The encoder plate is its lid, and its front wall rises to close the front of the bottom bay. The cables run down the right side (seen from the room), hidden behind the fans and held by three tie-wrap anchors on the back of the fan plate; the same anchors and a cable entry on the left side allow the mirror image. Power comes from two plug-in adapters: a 12 V brick for the fans and the motor, whose wire comes in with the other cables, and a 5 V USB-C adapter for the ESP32, whose cable passes through a diamond opening in the left wall. The layout drawing shows where everything goes on the perfboard.
  • Water stays out of the room. A 3 mm drip dam across the lower part of each bore, just before the fan, stops droplets from running indoors. Around the bottom shaft hole in each duct, a 16 mm boss rises 3 mm above the floor, so water flows around it instead of down the shaft. An optional drip tray under the unit, on the window ledge, catches anything that still drips; it evaporates there.
  • How it seals. The lips do the sealing, not the gap between the disc and the bore: the flat lip is 5.6 mm wide and overlaps the disc edge by 2 mm, and the conical lip meets the disc's bevelled rim over a band about 2.8 mm wide (the same 2 mm across the bore, along the 45° slope). Between them is 1.6 mm neoprene in a gap of about 1.5 mm, so it is only lightly squeezed: the disc's two seats are relieved for it (see cutting the lip foam). The disc is 67 mm in a 74 mm bore so it can be slid in edge-on past the dam and the bosses. Around the shaft, where the lips must stop to clear the hub, a boss at the top and the one at the bottom close the gap. Their inner faces are flat, square to the shaft, and the disc's two shaft ends are cut to matching flats: a flat end turning about the shaft stays in its own plane, so the gap is 0.2 mm at every angle.

Top-down section through the upper duct: disc closed against the two half-ring lips, at 45 degrees, and fully open

Cutaway of a closed damper: the disc (orange) sits between two red half-ring lips, one on each side of the shaft and on opposite faces of the disc; the floor boss surrounds the bottom shaft hole

Looking along the duct at the closed disc: flat shaft bosses at the top and bottom face the disc's two flats, and the lip behind the disc covers the rest of the gap

Side section through the shaft plane: drip dam at the fan end, raised boss around the bottom shaft hole, disc on the shaft, O-ring spigot at the foam end

Estimates (one sleeper)

Airflow (each way)CO₂ in the roomHeat lost at −20 °C outdoors
5 L/s (11 CFM)~1000 ppm~250 W
8 L/s (17 CFM)~800 ppm~400 W
11 L/s (23 CFM)~700 ppm~540 W

At 8 L/s, roughly 10 hours a day over a cold-climate heating season (about 200 days, electricity at 7–11 ¢/kWh), the lost heat costs about $35–45. The fans use about 1 W each.

Printed parts

Part
Thumbnail
QtyDescriptionSize (mm)Print orientation
body1Housing: two ducts with the damper lips, drip dams and shaft bosses, spigots for the foam, fan plate, motor frame and encoder plate90 × 241 × 114Fan plate on the bed, no supports
disc2Butterfly damper disc, clamped to the shaft by two M3 grub screws; the half engraved B has a 45° bevel on its rim. Its seats are relieved for the 1.6 mm lip foam, and its two shaft ends are cut flat to face the bosses67 × 65 × 14Plate on the bed, hub up
disc_nofoam(2)Instead of disc, for lips without foam: half A is 3.6 mm thick and half B's bevel is smaller, so both seats close to 0.3 mm from the bare lips67 × 65 × 14Plate on the bed, hub up
motor_plate1The motor screws to it on the bench; it then sits on top of the housing: two screws in front of the motor, and two tabs at the back, down through the frame and screwed to the tower walls56 × 61 × 15Top face on the bed, tabs up
magnet_cap1Holds the encoder magnet on the bottom end of the shaft; one M3 grub screw, height adjustable ±1 mm14 × 14 × 12Bore down
ebox1Electronics box under the housing; its front wall also covers the front of the bottom bay, and two back tabs screw to the bay walls80 × 76 × 64Floor on the bed
tube_spacer1Flat "8" over both spigots, against the stop rings and squeezed by the foam: it holds the spigots' ends at their spacing, so foam holes slightly off 96 mm can't bend the ducts and bind the shaft. The vent sits 2.5 mm further out of the foam90 × 186 × 2.5Flat
screen_frame1Insect screen frame on the foam's outdoor face: the mesh is pressed into a groove around both holes, two sleeves plug into the holes, four screws in the corners, a drain at the bottom of each hole94 × 193 × 15Outdoor face on the bed, sleeves up
tray(1)Optional drip tray on the window ledge, under the whole unit: the electronics box stands on ribs above its floor, and an adjustable leg in each corner levels it112 × 115 × 22Floor on the bed
tray_feet(4)The tray's feet, one per leg: each slides in its post's socket, carries a captive M3 nut, and has a groove for the M2 stop9 × 9 × 20 eachOn its side, the nut's slot up

Housing and discs must match. The housing's shaft bosses have flat inner faces, and the discs have matching flats. A housing printed before this change (curved boss faces) needs the earlier discs: set flat_boss = false in cad/window_vent.scad and export disc (or disc_nofoam). A disc with flats leaves a 1.6 mm gap at a curved boss, and a disc without them won't turn between flat bosses.

For a 180 mm printer (e.g. Bambu Lab A1 mini), print the housing in two halves instead of body. The cut runs midway between the two ducts, where only the fan plate joins them; a small splice plate behind the fan plate joins the halves again with four M3 screws, driven from the fan side in the band between the two fans. Each half prints like the full housing, in about half the time (roughly 5–6 h), so they can go on different days; use the same spool and profile for both.

Part
Thumbnail
QtyDescriptionSize (mm)Print orientation
body_top1Upper half: intake duct, upper fan, motor frame, and both middle tie-wrap anchors (whole)90 × 130 × 114Fan plate on the bed, no supports
body_bottom1Lower half: exhaust duct, lower fan, encoder plate; notched under the two middle anchors90 × 116 × 114Fan plate on the bed, no supports
splice1Joins the halves behind the fan plate; four M3 nut pockets on its back60 × 32 × 6Flat, nut pockets up

Exploded view of the split housing: upper and lower halves, the splice plate in the gap between the ducts behind the fan plate, and the four screws in front

Optional test prints, before the housing. Print the small real parts first (discs, motor plate, magnet cap, about 1.5–2 h): they test the rod, the grub screws, the motor and the magnet. Then:

Part
Thumbnail
Size (mm)TimeWhat it tests
fittest60 × 34 × 22about 20–30 minThe housing's printer-sensitive features, in its print orientation, engraved A to G (see below)
oring_testØ 90 × 18about 30 minOne spigot section (with an O-ring groove): push it into a test hole cut in a foam scrap, bare first; if it's loose, try it with an O-ring

The fit-test coupon. It copies seven features of the housing that depend most on the printer: holes and a slot, a nut pocket, a pilot for self-tapping screws, the conical lip's 45° ledge and a countersink. Each is engraved with a letter, A to G. Print it with the housing's filament and settings, then check each feature against the table below: if one fails, adjust the slicer (or the hole sizes in the model) before starting the 9–12 hour housing print.

Fit-test coupon with its features engraved A to G

FeatureCopied fromPass if
AM3 hole + nut pocketfan platethe nut drops in and stays; an M3 screw passes freely
BØ 2.5 self-tapping pilotthe box cover bossesan M3 screw cuts a firm thread without splitting the post
C5.6 mm ledge with a 45° underside (on the back of the plate)conical lip B in each borethe slope is smooth, not drooping or stringy
DØ 5.4 shaft hole, horizontal, with its sacrificial finbearing holes in the duct wallsthe top of the hole stays round; after drilling the fin out (see below), the 5 mm rod turns freely
EAS5600 diagonal slot (3.5 wide) on a 2 mm standoffencoder platean M3 screw passes at both ends of the slot
FM3 hole, horizontalmotor framean M3 screw passes freely
GM3 hole with a 90° countersink, opening on the bedsplit housing joint (body_top and body_bottom)an M3 flat-head screw sits flush or just below the surface

PETG. No support is needed anywhere: every face that looks down when printing (the lip B seat, the disc bevel, the boss sides, the O-ring grooves, the countersinks) slopes at 45° or less.

Filament

About 0.5 kg of PETG for one unit (about half a 1 kg spool), or roughly $12–17 at $25–35/kg.

PartQtySolid volume
body1282 cm³
disc22 × 12.7 = 25 cm³ (disc_nofoam: 2 × 14.7)
motor_plate112.6 cm³
magnet_cap11.6 cm³
ebox147 cm³
tube_spacer14.5 cm³
screen_frame139 cm³
Total≈ 412 cm³

The split housing (body_top 136 cm³, body_bottom 145 cm³, splice 8 cm³) adds about 7 cm³ (9 g).

  • Printed fully solid, 412 cm³ of PETG (1.27 g/cm³) weighs about 523 g.
  • Infill hardly matters: almost every wall, plate, lip and disc is 6 mm thick or less, so with 4–6 perimeters the slicer prints it solid anyway. Expect about 470–520 g, plus a little for the skirt or brim and purging.
  • The optional test prints add about 35 cm³ (44 g): fittest 12 cm³, oring_test 20 cm³, fin_test 3 cm³. The optional drill_guide adds 9 cm³ (11 g), and the optional lip foam cutting guides 9 cm³ (12 g): foam_guide_a 4.3 cm³, foam_guide_b 5.0 cm³. The screen frame's screen_test adds 6 cm³ (7 g). The optional drip tray adds 46 cm³ (58 g): tray 41 cm³, tray_feet 5 cm³, and its tray_test 5 cm³ (6 g).
  • Print time: very roughly 9–12 hours for the housing on a Bambu Lab P1 (0.2 mm layers; about 7–9 hours at 0.28 mm), and about 3–4 hours for all the other parts together. The slicer gives the exact figures.

Slicer settings

Most parts are thin, so the number of walls (Bambu Studio: Strength → Wall loops) matters more than the infill percentage: with enough walls, the thin parts come out solid and the infill only fills the few thicker zones.

PartWall loopsSparse infillWhy
body (or body_top and body_bottom)425 %Thin walls everywhere; the infill only reaches the thick zones (bosses, spigot roots, fan plate). No supports: a sacrificial fin holds up the top of each of its four shaft holes (see below)
disc (or disc_nofoam)540 %The grub screws thread into the hub: 5 loops leave about 2 mm of solid plastic around each hole
motor_plate540 %Screw holes, bosses and tabs under clamping load
magnet_cap100 %Tiny, with a grub screw in it: print it solid
splice540 %Joins the two halves: nut pockets and countersunk screws
ebox415–20 %No real loads; a 4th wall (same print time) makes the walls around the screw holes stronger
tube_spacer100 %Thin rings (3 mm wide, 2.5 mm thick) that must not stretch: print it solid
screen_frame, screen_test425 %Thin plate and sleeves: 4 walls print them nearly solid, so the groove's walls hold the spline
tray, tray_feet, tray_test425 %The 5 bottom layers below keep the tray's 1.6 mm floor watertight; 4 walls make the posts and feet nearly solid
fittest, oring_test425 %As the housing, so they test what the housing will be
drill_guide440 %Small; 4 walls make the bushings solid
foam_guide_a, foam_guide_b440 %Thin plate and rib: 4 walls print them solid, so they stay flat and stiff
louver_chassis320 %Light load
louver_blade315 %Thin and light; their pins come out solid anyway
louver_link_nopad (or louver_link), louver_retainer100 %Small and thin
  • Infill pattern: gyroid. Equally strong in every direction, and it prints smoothly in PETG.
  • Top and bottom shell layers: 5. Flat faces stay smooth and the infill doesn't show through.
  • Supports: none. Every part is designed to print without them. The housing's four shaft holes print horizontal, so each one carries a sacrificial fin: a thin lattice of X's (0.8 mm, two nozzle lines; fin_t in the model, 0 for none) standing on the bottom of the hole and holding up its top, with 1.5 mm clear at each end. After printing, drill the fins out by hand with a 5.2 mm bit (13/64 in, the one used on the housing; 5.4 or 5.5 mm also works): the bit enters the clear end first, sits square, and cuts the fin easily. Tested on the housing: the holes come out round, with no warping, and the 5 mm rod turns freely. Feature D of fittest is the same hole with the same fin, and fin_test is a 10-minute block with two of them.

Hardware

Prices: Amazon.ca, October 2026, before taxes. Packs of two leave a spare.

ItemQtyNotesPrice (CAD)
80 mm PWM fan, ≥ 2 mm H₂O static pressure2Noctua NF-A8 PWM (2.37 mm H₂O, 17.7 dB(A) max). Run at 30–60%2 × $24.95
Stepper motor, NEMA 17 pancake1StepperOnline 17HS08-1004S (42 × 42 × 20 mm, 5 mm shaft, 13 N·cm). Any NEMA 17 fits the mount$26.50
TMC2209 stepper driver1StealthChop for silent moves; UART for StallGuard$16.99 (pack of 2)
AS5600 magnetic encoder board + diametric magnet1Sold with the magnet (the kit: 23.3 × 22.8 mm board, 3.95 × 2 mm magnet). A 20 × 20 board with 15 mm hole spacing also fits: 4 × M3 × 12 + nuts onto standoff pads on the bottom plate. See the magnet note below$12.69 (pack of 2)
ESP32-S3 dev board1Freenove ESP32-S3-WROOM Lite (FNK0099), the board the firmware's pinout is written for; other ESP32-S3-WROOM-1 boards work with a pin check. Flash it over USB on the bench; the final firmware updates over Wi-Fi—
CO₂ sensor, NDIR1Sensirion SCD40 module (CO₂ + temperature + humidity, I²C). Not CCS811/SGP30 ("eCO₂" estimates)$37.45
DS18B20 temperature probe, waterproof1 (+1 optional)Outdoor air: the 6 mm stainless probe on its cable goes through the foam insert, above the intake (template). A second one, indoors on an inside wall, is optional: the SCD40 already reads the room's temperature—
Steel rod, 5 mm × 200 mm1Damper shaft. 200 mm fits as is, no cutting. If it is HSS (very hard), grind a small flat with a rotary tool where each disc and the coupler clamp, so the screws can't slip$12.79 (pack of 2)
Flexible shaft coupler, 5–5 mm, 19 × 25 mm1$13.49 (pack of 2)
M3 × 35 screws + M3 nuts8 eachFans—
M3 × 8 screws4Motor to motor plate, through the bosses under the plate. Not longer: the motor's holes are only 2.5 mm deep (section)—
M3 × 10 (or M3 × 12) screws + M3 nuts6 eachMotor plate: two through the frame in front of the motor, two through the tower walls and the plate's back tabs. Electronics box: two through the bay walls and the box's back tabs—
M3 grub screws, 6 mm (and one 4–5 mm)5Two 6 mm per disc, 24 mm apart along the hub, so the disc can't rock on the shaft (flush with the hub); one 4–5 mm in the magnet cap (a 6 mm one would stick out 1.5 mm). Self-tapping into the plastic—
O-rings, 74 × 3 mm, nitrile 70A4Optional, and not needed by default: the plain Ø 83.7 spigots are a snug fit in the hole (83.3 mm with the 3-1/4 in hole saw). For a hole that comes out loose, set orings = true before printing the housing: two grooves per spigot, sized for this O-ring (about 6% stretch, 20% squeeze in an 83.3 mm hole)$12.79 (pack of 10)
Insect screen kit: fibreglass mesh, 0.125 in screen spline, spline roller1Insect screen: a piece of mesh about 115 × 210 mm and about 50 cm of spline (or 3 mm O-ring cord, cut from the 74 × 3 O-rings above)—
Countersunk screws, 8 mm head (3.5 mm high), about 25 mm long4Screen frame into the foam (4.25 mm holes)—
M3 × 20 button head screws (ISO 7380, 2 mm hex) + M3 nuts4 eachOptional drip tray: the legs. Felt pads under the feet are optional too—
M2 × 6 flat head screws4Optional drip tray: the legs' stops, through each post's outer wall (self-tapping)—
XPS foam board, 2 in (51 mm), 30 psi1Foam insert, e.g. Owens Corning FOAMULAR C-300 (2 × 24 × 96 in, R-10). The 30 psi grade over the 20 psi (wall) one: stiffer under the housing's weight, it doesn't creep around the spigots, and the hole saw cuts it cleaner. Many boards have shiplap (SL) edges, stepped to overlap on a wall: cut the insert from the middle of the board, or saw the step off any edge that meets the window frame—
Closed-cell neoprene foam sheet, self-adhesive, 1/16 in (1.6 mm)1Lip seals, cut with the templates; not needed with disc_nofoam (without lip foam). The model leaves a 1.5 mm gap, so the foam is only lightly squeezed: closed-cell neoprene is too stiff for more
Perfboard (protoboard), 5 × 7 cm, holes 66 × 46 mm1Carrier for the ESP32 and the TMC2209 in the electronics box
Resistors and capacitor for the perfboard1 kΩ (TMC2209 UART), 10 kΩ × 5 (driver enable pull-up, two fan PWM pull-downs, two tach pull-ups), 100 kΩ + 22 kΩ (12 V sense), 4.7 kΩ (DS18B20 pull-up; × 2 with the optional indoor sensor), 100 µF ≥ 25 V electrolytic (TMC2209 motor supply). See firmware/README.md—
Female pin headers, 2.54 mm (optional)to suitSockets for the ESP32 and the TMC2209 instead of soldering them, so they unplug for replacement (esp_mount, tmc_mount; the box has room for both)
M2 screws, about 6 mm4Perfboard onto the box standoffs (self-tapping)
M3 × 12 countersunk (flat head, ISO 10642 or DIN 965) screws + M3 nuts4 eachSplit housing only: fan plate to the splice, heads flush in 90° countersinks—
M3 × 12 screws2Electronics box: through the front cover (self-tapping)—
Small tie wraps, 2.5–3.6 mm3+Cable anchors (three on each side; use one side)
12 V power brick1Fans and motor, about 1 A. Only its wire enters the box, to a 2-pin screw terminal on the perfboard—
USB-C power adapter, 5 V (1 A or more), and a USB-C cable1Powers the ESP32 through its USB-UART connector, which also gives the console. The cable passes through a diamond opening in the box's left or right wall (usb_port: or only a cross to drill, or nothing; usb_side: left, right or both)—
Total boughtNot counting the foam insert and foam sheet, or the two power adapters≈ $181

The stepper motor

The StepperOnline 17HS08-1004S, from its sheet and checked with a caliper. Its M3 holes are only 2.5 mm deep, so the motor plate has a 2 mm boss under each screw hole: through the 4 mm plate and the boss, an M3 × 8 reaches 2 mm into the motor. The bosses and the screw heads sit in the frame's diamond pockets.

17HS08-1004S stepper, dimensioned: 42.3 mm square frame, 20.5 mm long; M3 holes 31 mm apart, 2.5 mm deep; 22 mm centring boss, 2 mm high; 5 mm shaft, 20 mm long, with a 4.5 mm D-flat along its last 16.5 mm; 400 mm leads

Section through a motor screw: motor end cap with a 2.5 mm deep M3 hole, 4 mm motor plate, 2 mm boss in the frame's 8 mm diamond pocket, 3 mm frame; an M3 x 8 button head reaches 2 mm into the motor, 0.5 mm short of the bottom, its head 0.65 mm below the frame

Its four leads, from the sheet, and where they go on the controller PCB's motor header J4 (silkscreen 1A, 1B, 2A, 2B; the TMC2209's M1A, M1B, M2A, M2B). Each coil is a pair: about 3.7 Ω between its two leads, nothing between the pairs, which an ohmmeter confirms before plugging in.

J4 pinSilkscreenDriver outputLeadCoil
1 (square pad)1AM1ABlackA
21BM1BGreenA\
32AM2ARedB
42BM2BBlueB\

If the motor turns the wrong way, swap the two leads of one coil (black and green, for example). Never plug or unplug the motor with 12 V on: it can destroy the driver.

The AS5600 board, a magnetic rotary position sensor

The kit board, measured. Its four mounting holes form a 16.7 × 16.2 mm rectangle (not a square); the diagonal slots in the encoder plate take it as well as the smaller 20 × 20 boards. Mount it chip up, under the magnet, and solder the wires straight into its holes: they run inward under the board, in the 2 mm the standoff pads leave, and down through the diamond window under the chip.

AS5600 kit board, dimensioned: 23.3 x 22.8 mm, four 3.6 mm holes 3.3 mm from the edges, a 3-pin row (VCC, OUT, GND) and a 4-pin row (DIR, SCL, SDA, GPO) 5.2 mm from the sides, chip in the middle; 1.6 mm board, 3.22 mm over the chip; the 3.95 x 2 mm diametric magnet 1 mm over the chip

The ESP32-S3 board, the Wi-Fi microcontroller

The Freenove ESP32-S3-WROOM Lite, measured. Its two pin rows are 25.3 mm apart, so they land 10 holes apart on the perfboard. It has two USB-C connectors at one end: plug the cable into the USB-UART one (power and console). The module's antenna sticks out 7 mm beyond the other end.

Freenove ESP32-S3-WROOM Lite, dimensioned: PCB 50 x 28.5 mm, antenna 7 mm beyond it over the middle 18.5 mm, two rows of 20 pins 25.3 mm apart, USB-OTG and USB-UART connectors 9 mm wide at the USB end; side view: 1.62 mm PCB on a 2.78 mm pin spacer, 5.54 mm pins, 4.85 mm to the top of the tallest part

The TMC2209 module, a silent stepper motor driver

A StepStick-format driver, measured: 20.81 × 15.08 mm, two rows of 8 pins 12.7 mm apart (5 holes on the perfboard). The chip is on the underside; its heatsink (15 × 14.4 mm, fins along the pin rows) sticks onto the thermal pad on top, toward the DIR/GND end so it clears the potentiometer, and stands 14.70 mm from the board's underside.

Check which pin carries the chip's UART. Some modules, such as the TMC2209-LA, have a 0 Ω link that routes it to either the PDN pin (pin 4) or the UART pin (pin 5), and come with it on PDN; the controller uses both. On the PCB, since v1.3, two 0 Ω 0402 jumpers on the back, JP3 (pin 4) and JP4 (pin 5), put the UART line on both pins, so either kind works; leave one out if a module uses that pin for something else. On an older PCB or the perfboard, nothing else uses pin 4, so a short wire between pins 4 and 5 on the board's underside does the same (the firmware README's bring-up, step 5, shows how to tell).

TMC2209 module, dimensioned: PCB 15.08 x 20.81 mm, two rows of 8 pins 12.7 mm apart (EN, MS1, MS2, PDN, UART, CLK, STEP, DIR; VM, GND, M1B, M1A, M2A, M2B, VIO, GND), heatsink 14.4 x 15 mm outline; side view: 1.62 mm PCB on a 2.62 mm spacer, 5.8 mm pins, 13.08 mm heatsink

The SCD40 board, the CO₂ sensor

The Sensirion SCD40 on a small 13.70 × 21.70 mm board, measured: 7.85 mm from its underside to the sensor's top, four pin holes 2.54 mm apart along the bottom edge (GND, VDD, SCL, SDA). It carries its own I²C pull-ups (R1, R2, 10 kΩ), so the controller needs none for it. On the controller PCB, J5 has the same pin order, so a straight 4-wire cable joins them. Keep the sensor's membrane uncovered (see the warning below).

SCD40 module, dimensioned: board 13.70 x 21.70 x 1.62 mm, 7.85 mm overall height, the sensor about 10.1 mm square near the top, four pin holes 2.54 mm apart along the bottom edge labelled GND, VDD, SCL, SDA, pin 1 hole 2.63 mm from the left edge and 1.00 mm from the bottom edge

[!WARNING] SCD40: do not remove the white membrane on top of the sensor. It looks like a small sticker, but it is part of the sensor: a gas-permeable filter that lets ambient air reach the photoacoustic NDIR cell while keeping dust, splashes and other contaminants out. Do not press on it, cover it, or clean near it with solvents or compressed air.

The MAX9814 board, the microphone (optional)

For the audio mode, measured: 25.6 × 14.3 × 1.62 mm, 9 mm from its underside to the capsule's top, and parts up to about 3 mm on the back. The 9.70 mm capsule sits 2.10 mm from one end, centred across the board; the five header holes (GND, Vdd, Gain, Out, AR) run along the other end, and two mounting holes flank the capsule. Tie Gain to Vdd (40 dB) and leave AR open. It lies capsule down in the microphone tube.

MAX9814 module, dimensioned: board 25.6 x 14.3 x 1.62 mm, 9 mm from its underside to the capsule's top, parts up to about 3 mm on the back; the 9.70 mm capsule 2.10 mm from one end, centred; five header holes 2.54 mm apart (GND, Vdd, Gain, Out, AR), 1.0 mm from the other end; two 2.30 mm mounting holes, 1.0 mm from the end and 1.5 mm from the long edges

The electronics box layout

The box seen from above, to scale, with the room at the bottom. The ESP32 lies across the back half, its pins soldered on the perfboard's grid, with its USB-end pins one column in from the board's edge so they clear the corner standoff; the TMC2209 sits at the front right, next to the 12 V terminal and the cable entry. The ESP32 can also be turned 180° in the same place, USB end toward the right wall: the box has a diamond opening on each side (views ebox_elec and ebox_elec_alt in the model). Keep wires and copper away from the antenna's end.

Electronics box from above, to scale: the perfboard on four standoffs; the ESP32 across the back half, its USB-C plug through a diamond in the left wall; the TMC2209, 12 V terminal and capacitor at the front right by the cable entry; fan, motor and sensor connectors and resistors at the front left; a second diamond in the right wall for the ESP32 turned the other way

Firmware

The ESP32 runs Arduino sketches (esp32 core 3.x). The first, firmware/baremetal/, is a serial command console that tests each electronic part on its own: fans, motor and driver, encoder, CO₂ sensor, temperature probes and the 12 V supply, and optionally a microphone's sound level (audio mode). The second, firmware/webui/, is the application: the CO₂ control, with its own web page on the home network for the live readings, graphs, settings and manual control, and a simulation that stands in for the sensors until they're connected. Both compile cleanly, and the web application runs on the bare ESP32 board in simulation, and the bare-metal console has now checked every part on the bench. firmware/README.md has the board settings, the wiring diagram, a part-by-part bring-up, the command list and the web application.

Wiring diagram: SCD40, AS5600, 12 V sense divider and both fans on the left of the ESP32-S3, TMC2209, stepper, two DS18B20 temperature sensors and the optional MAX9814 microphone (audio mode, with its GPIO 21 to GPIO 3 wire) or INMP441 I2S microphone (SD on GPIO 21, SCK and WS on GPIO 38 and 39) on the right, 12 V brick and 5 V USB-C adapter on top

Building the perfboard. The build plan puts this wiring on the 5 × 7 cm protoboard: the ESP32-S3 and the TMC2209 on their grid positions in the box, the headers, resistors, capacitor and 12 V terminal around them, and 61 numbered wires on the underside, each listed with both of its holes. A PDF version splits it over three landscape Letter pages for the bench: the component side, the solder side and the wire list. Solder the parts and the wires that run under the modules first, then the two modules.

Perfboard build plan: the component side seen from above with the ESP32-S3, the TMC2209, headers J1 to J8, resistors R1 to R10 and C1 on a 24 x 18 grid; the solder side with 61 numbered, colour-coded wires; and the wire list

Or a printed circuit board. The same circuit also exists as a 2-layer PCB of the protoboard's size (70 × 50 mm, 1.6 mm) with the same four M2 holes, so it drops into the box in its place: the ESP32-S3 and the TMC2209 sit on exactly the same pins, and every connector keeps its function. It adds a ground plane on both layers, 0.8 mm tracks for 12 V and the motor, a 1N5822 against a reversed 12 V plug (a jumper on JP1 bypasses it), test pads for GND, 3V3, VIN, 12 V and the TMC2209's DIAG, and since v1.1 a jumper on the back, JP2, that joins GPIO 21 to GPIO 3 for the firmware's audio mode (on v1.0, a short wire does it). Since v1.2, the 1-Wire pull-ups R9 and R10 lie flat on the back: on v1.1 they stood in the path of the ESP32's second USB-C plug (the unused USB-OTG one). Since v1.3, the jumpers are 0402 pads for a 0 Ω resistor or a short wire (JP2 was a solder jumper), and JP3 and JP4 put the TMC2209's UART line on both pins 4 and 5. Since v1.4, J7 is a 2 × 3 header for an INMP441 I²S microphone: its first row is the old 3-pin header (3V3, IN, GND) for the indoor DS18B20 or the MAX9814, and the second row adds WS (GPIO 39), SCK (GPIO 38) and a GND for the microphone's L/R pin, with SD on IN (GPIO 21). To make room, the motor header J4 stands beside the TMC2209, the SCD40's J5 takes J4's place on the bottom row, and R3 and R4 sit under J7. Five boards cost US$10 to 32 at PCBWay with shipping, depending on its speed: upload the Gerber zip and keep the standard options (2 layers, FR-4, 1.6 mm, 1 oz, HASL lead-free). The schematic and the KiCad 10 project are in pcb/; the scripts tools/pcb_*.py build all of it, Freerouting doing the routing.

The controller PCB from above: the ESP32-S3 footprint across the back half with the antenna area clear on the right, the TMC2209 below it, the 12 V input with D1, JP1 and C1 at the bottom right, the labelled connectors for the fans, SCD40, AS5600, both DS18B20 and the microphone (J7, 2 x 3) at the bottom left, and the motor header standing beside the TMC2209 The controller PCB from below: the ground plane, the title WINDOW VENT CONTROLLER v1.4, the orientation sketch, the audio-mode jumper JP2 and the UART jumpers JP3 and JP4

PCB bill of materials
RefQtyValuePart / note
U11Freenove ESP32-S3 Lite (FNK0099)see Hardware
U21TMC2209 modulesee Hardware
D111N5822Schottky, 3 A 40 V, DO-201AD, standing
C11220 µF 50 Velectrolytic, 10 mm, 5 mm pitch
R1–R4410 kΩ1/4 W, standing
R5–R8410 kΩ, 1 kΩ, 100 kΩ, 22 kΩ1/4 W, lying flat, component side: body 2.5 mm (max), otherwise: solder side
R9, R1024.7 kΩ1/4 W, lying flat on the solder side (v1.2; standing on v1.1); leave R9 out in audio mode (or without the indoor probe)
J1, JP122-pin headerplus a jumper shunt for JP1
J2–J654-pin header2.54 mm male
J712 × 3 header2.54 mm male (v1.4; a 3-pin header before)
J813-pin header2.54 mm male
JP20 Ω 0402on the back; fitted (or a wire) in audio mode only
JP3, JP420 Ω 0402on the back, under the TMC2209's pins 4 and 5; or a short wire each

The encoder magnet

The AS5600 measures the direction of the magnetic field across the shaft, so the magnet's north-south axis must lie across the shaft and turn with it.

  • A diametric magnet (magnetized across its diameter) does this lying flat on the shaft end. AS5600 boards are usually sold with one: this kit's measures 3.95 × 2 mm, others 6 × 2.5 or 6 × 3 mm. This is the default (magnet = "diametric", magnet_d = 4, magnet_h = 2); set them to the one you have.
  • An ordinary small cylinder magnet is usually axial (poles on its flat ends). Lying flat it would not work, but lying on its side it does: set magnet = "sideways" for a cap that holds a 2 × 2 mm cylinder across the shaft. A magnet that small may be too weak at 1–2 mm; the AS5600's status register reports "too weak / OK / too strong", so check it once.
  • Without any magnet, the TMC2209's StallGuard can find "closed" by sensing the stall against the lips.

Cutting the lip foam

Full-size templates for the foam on the four lips (two per duct). The flat lip takes a half-ring; the conical lip takes the cone unrolled, a flatter arc that lies on the 45° slope without wrinkles. Print the PDF at 100 % (Actual size, not Fit to page), check its 100 mm and 50 mm bars with a ruler, then trace the shapes on the foam's backing paper and cut.

Preview of the lip templates page: two half-rings for the flat lips, two unrolled arcs for the conical lips, and 100 mm and 50 mm scale bars

Cutting guides (optional). Rigid printed copies of the two templates, foam_guide_a and foam_guide_b. The plate is the strip's exact outline: press it on the foam by its finger grip, which holds the sheet still, and run a craft knife along its edges, blade upright against the plate. The grip and the rib under it stay inside the edges, clear of the blade, and the rib stops short of the ends, so the knife can cut across them. Cut with the backing paper up (it slips less than the bare foam), in two or three light passes. Tested: much easier, faster and more accurate than cutting around the paper templates. Leave the slicer's elephant-foot compensation on: the blade runs against the plate's bottom edge.

Part
Thumbnail
Size (mm)TimePrint orientation
foam_guide_a73 × 30 × 18about 15–20 minPlate on the bed, grip up
foam_guide_b87 × 27 × 18about 15–20 minPlate on the bed, grip up

Cutting the foam neatly takes some care: a sharp blade and a steady hand, and a second try is not unusual. Tweezers help to lay the narrow strips on the lips inside the ducts. If that sounds like too much, see without lip foam below.

How the gap is set. The housing places the lips for a 1 mm gap (gasket = 1), but 1.6 mm neoprene squeezed that much is too stiff: even a finger can't close the disc on it, and light shows through. So the discs make the room instead, and the housing never changes. seat_rlf moves each of the disc's two seats: [0] lowers half A's face (or raises it, when negative), [1] sets half B's bevel back square to the slope (or forward). The gap at each seat is gasket + seat_rlf, less 0.1 mm on A and plus about 0.07 mm on B: the grub screws push the shaft into one side of its bore, which moves the disc 0.1 mm toward lip A. With foam, seat_rlf = [0.6, 0.5] gives gaps of about 1.5 mm on both seats, so the foam is only lightly squeezed (half A's face lands at 2.4 mm, a whole number of 0.2 mm layers).

Without lip foam

disc_nofoam closes on the bare lips instead, so there is nothing to cut or stick: print it instead of disc and skip the foam. In the model, lip_foam = false does the same (seat_rlf = [-0.6, -0.8]): half A is thickened to 3.6 mm and half B's bevel shrinks, leaving gaps of about 0.3 mm on both seats. The housing is the same either way, so you can switch later by printing the other discs.

The trade-offs:

  • It leaks more. Plastic on plastic with a 0.3 mm gap seals far less well than foam: expect a slight draft, and light around the lips, when closed. For a vent that only needs to stop most of the airflow, that may be enough; in a cold climate, or on a windy side of the house, prefer the foam.
  • It's noisier. The disc closes on hard plastic with a small click, and nothing damps the fans' vibration at the lips.
  • Closing is less forgiving. The foam makes up for small print errors; bare lips don't. The motor stops on the plastic: the encoder (or StallGuard) sees it.

To adjust (open cad/window_vent.scad in OpenSCAD, change seat_bare, export part = "disc_nofoam"):

  • The disc jams before it closes, or rubs: increase both values toward zero ([-0.4, -0.6] gives about 0.5 mm). Change them in steps of 0.2 mm on half A, so its face stays on a 0.2 mm layer.
  • Too much light or draft: make them more negative, 0.1 mm at a time (on B: -0.9), but below about 0.2 mm the disc risks binding on a slightly warped print. Print one disc and try it before printing the second.
  • Check after any change: python tools/check.py vent runs the swing, stop and fitting checks for both discs.

Drilling the foam insert

The insert takes two 83 mm holes, 96 mm apart, intake on top. The housing's plain Ø 83.7 spigots are a snug fit in them (the saw measures 83.3 across its teeth, so its holes come out about 83.3), so no O-rings are needed (sand a tight hole a little). Print the drilling template at 100 %, check its 100 mm bars, tape it on the foam centred across its width, and mark both centres with an awl. The template also marks an optional hole for the outdoor temperature probe, above the intake.

Preview of the drilling template: two 83 mm circles 96 mm apart, labelled intake and exhaust, with centre crosses, the stop ring outlines, 100 and 112 mm width guides, an optional probe hole above the intake, and 100 mm scale bars

Straight pilot holes (optional). A pilot hole that leans sends the hole saw off its line on the far face. The printed drill_guide keeps the bit perpendicular: two 20 mm bushings, A for the hole saw's pilot holes (5.2 mm bit, or 13/64 in: slightly smaller than the saw's 5.8 mm pilot drill, which then follows the hole) and B for the probe's hole (6 mm, or 15/64 in: snug on the 6 mm probe), set in guide_d to suit your bits. Lay it on the taped template: the notches at both ends on the vertical centre line, and the side notches of the bushing you use on that hole's cross line. Hold it down and drill straight through the paper and the foam; then move it to the next hole. The bit has to reach through 20 mm of bushing and 51 mm of foam, so use one with at least 75 mm of flute (a 6 in aircraft bit, for instance). Run each bit through its bushing once before the first hole, to size the bore.

Part
Thumbnail
Size (mm)TimePrint orientation
drill_guide26 × 46 × 20about 20 minBase on the bed, bushings up

A 3-1/4 in (83 mm) bi-metal hole saw cuts only 32 mm deep, less than the 51 mm foam. Two ways to get through:

  • From both faces (easiest): drill the pilot hole right through at each centre (a skewer or a long nail finds it on the far face if the pilot drill is too short), cut halfway from one face, turn the foam over and finish from the other. The two cuts meet with about 13 mm of overlap, and neither face tears out.
  • From one face: cut 32 mm deep, carve the plug out (crossed cuts down to the groove, then pry the pieces out), and cut on: the pilot hole, already deeper, keeps the saw centred. Clamp a scrap board under the far face so the foam doesn't tear where the saw comes out.

Run the drill slowly. Hole saws often cut a little wider than labelled, so cut a test hole in a scrap first and try oring_test in it. A hot-wire foam cutter also works, and leaves the smoothest bore.

83 mm hole saw: 32 mm cutting depth, pilot drill leading the teeth, 98 mm arbor; and a section of the 51 mm foam cut 32 mm from each face, with 13 mm of overlap

Insect screen

A printed frame holds a fibreglass insect screen on the foam's outdoor face, over both holes. The mesh is pressed into a groove around the frame's edge by screen spline, the way a window screen is made, so it comes out for cleaning or replacement: pollen and fluff build up on the intake side, and every clog costs airflow. Two short sleeves on the frame's back plug into the holes' outdoor ends (the housing's spigots fill only the room-side 25 mm of the foam): they centre the frame, hold it, and keep insects from getting in behind it. Four countersunk screws in the corners keep it in place. At 94 mm wide, it fits between the caps' lips, flush with them. Its top edge is 95 mm above the vent's centre, clear of the probe's hole at 102 mm.

Each sleeve has a 15 mm drain slot at the bottom, through its wall and the plate. Condensation runs along the hole's floor, so without the slot it would pool against the sleeve's end and soak into the foam. The slot's floor is flush with the hole's floor and drops 1 mm toward the outdoor face, so the water runs out under the mesh. The exhaust hole, the lower one, carries warm, damp room air out into the cold, so it gets the most.

Part
Thumbnail
Size (mm)Print orientation
screen_frame94 × 193 × 15Outdoor face on the bed, sleeves up
screen_test94 × 27 × 15Outdoor face on the bed

Try the groove first. screen_test is the frame's bottom end: an arc of the groove, two screw holes and an arc of sleeve with its drain. The 2.8 mm groove has been tested with 0.125 in screen spline over fibreglass mesh, and it works well. The O-ring cord hasn't been tried yet, and other splines vary, so roll yours over a scrap of mesh into the test piece first: it should go in with a firm push of the roller and not pop out when the mesh is pulled. If it is loose or too hard to roll in, change the groove's width, scr_groove[0] (2.8 mm), in cad/window_vent.scad. Try the screws in a foam scrap at the same time.

Fitting the mesh.

  1. Lay the frame on the table, outdoor face (the groove) up. Cut the mesh about 10 mm larger than the frame all round.
  2. Start the spline at the bottom, in the middle of the straight part. Roll it in along one side and around the top, keeping the mesh just taut, then down the other side and back to the start. Cut the spline so its two ends butt.
  3. Trim the mesh along the groove's outer edge with a sharp knife.

Fitting the frame. Push the sleeves into the holes from outdoors, then put the four screws through the corners into the foam. Screws about 25 mm long reach 20 mm into the foam, which is plenty, since the sleeves carry the frame.

Drip tray

An optional tray on the window ledge, under the whole unit, from 6 mm off the foam (clear of the bottom cap's lip) to just past the fans. It catches drips of condensation, which evaporate indoors, and it levels the unit on a ledge that isn't flat. The electronics box, the unit's lowest part, stands on seven 3 mm ribs, so it stays above any water. The ribs are evenly spaced across the tray, 13.1 mm apart, and stiffen its thin floor under the unit's weight, the outer two under the box's side walls. Each stops 2 mm short of the walls at its ends, so water can pass from one channel to the next; the outer two pass close to the corner posts, so they stop 2 mm short of the posts instead. At 112 mm wide, the tray reaches 15.9 mm past the fans on each side, so it needs about 16 mm or more beside the fans: a window frame's wall 20 mm away leaves about 4 mm.

A leg in each corner sets the height. Each one is an M3 × 20 button head screw and a square foot with a captive nut, which slides in the corner post's socket. The screw goes in from below, and its head presses up into a 45° pyramid that closes the top of the socket. Turning the screw from above, with a 2 mm hex key through the hole in the post's top, moves the foot up or down: 0.5 mm per turn. An M2 × 6 flat head screw, through the post's outer wall (3.5 mm thick, for its thread), rides in a groove up the foot. It holds the leg in when the tray is lifted, and the groove's ends stop the foot, so the floor's underside sits 4 to 11 mm above the ledge. The posts stand beside the electronics box, inside the tray's outline.

Print without supports. Nothing in the posts or the feet needs one: the pyramid's faces slope at 45°, and the feet print on their side with the nut's slot and the groove facing up. Supports inside a post can't be removed (tried), and the first design's flat ceilings, bridged without supports, sagged too much for the screw's head.

Part
Thumbnail
Size (mm)Print orientation
tray112 × 115 × 22Floor on the bed
tray_feet9 × 9 × 20 each, fourOn its side, the nut's slot and the groove up
tray_test44 × 19 × 22Floor on the bed, the foot on its side

The unit standing on the drip tray on the window ledge: the electronics box on the tray's ribs, a leg in each corner of the tray

Try a leg first. tray_test is one corner of the tray, with its post, and a foot. Fit a screw, a nut and the M2 stop as below and check that the foot slides freely without wobbling, that the screw turns easily under a little load, and that the stop holds the foot at both ends of its travel. Then fill the whole tray with water once, before trusting it.

Fitting the legs.

  1. Slide a nut into each foot's slot, under its 1 mm cap.
  2. Push a screw up into a post's socket from below, head first, until its head sits in the pyramid at the top.
  3. Push a foot up into the socket, its slotted, grooved face toward the post's outer wall, and turn the screw from above to thread it on. Bring the foot to about the middle of its travel.
  4. Drive the M2 × 6 through the small countersunk hole in the post's outer wall, into the foot's groove. It cuts its own thread in the plastic: snug it, then stop, or it strips.

Levelling. Set the tray on the ledge with the feet at about 7 mm. Level it across and along with three legs, then bring the fourth down until it just starts to resist the key: four legs on a ledge that isn't flat, and no rocking. A slight tilt toward the foam (about 1 mm lower there) also helps the ducts drain outward.

The foam holes come after the tray. The tray sets the unit's height, so drill the holes to match it: stand the unit on the levelled tray against the foam, mark the hole centres from it, then drill. Keep the unit below the window's latch zone.

Assembly

  1. Split housing only: press four M3 nuts into the splice's pockets. Stand the lower half upright, cut face up, hold the splice against the back of its fan plate, just above the duct, and fit the two lower countersunk M3 × 12 from the fan side. Lower the upper half onto it and fit the other two. Leave all four slightly loose until step 3.
  2. Line the lips inside the ducts with foam cut from the templates: a half-ring on each flat lip, an unrolled arc on each conical lip (B). With disc_nofoam, skip this step.
  3. Slide each disc edge-on into its duct from the fan end, hub toward the room, with the half engraved B on the side of the conical lip (the right side, seen from the room).
  4. Drill the fins out of the four shaft holes if not done yet (see Printed parts). Push the shaft down through the frame on top, both ducts and both disc hubs. Split housing: the shaft now lines up the two halves; tighten the four splice screws.
  5. Turn both discs closed against the lips. Centre each disc between its two bosses: slip a 0.2 mm shim (a feeler gauge, or two layers of printer paper) between each flat and its boss, then tighten the grub screws (two per disc) through the duct openings and pull the shims out.
  6. Screw the AS5600 board onto the four standoff pads, chip up, with its wires soldered into its holes and led inward under the board, then down through the diamond window under the chip. Glue the magnet into the cap and slide the cap onto the shaft's bottom end. Lay a 1 mm shim on the chip (a plastic card works), slide the cap down until the magnet rests on it, tighten the cap's grub screw, and remove the shim.
  7. On the bench, screw the motor to the motor plate (4 × M3 × 8 from below) and fit the coupler on the motor shaft.
  8. Lower the motor and plate onto the top of the housing, with the coupler through the big diamond and over the shaft end, and the plate's two back tabs down through the slots in the frame. In front of the motor, put an M3 × 10 down through the plate and the frame, with a nut underneath, reached from the tower's open front above the fan plate. At the back, put an M3 × 10 through each tower wall and its tab from outside, with a nut on the inside, reached from the tower's open back. Then tighten the coupler's lower screw through a side window.
  9. Fit the fans: the top one blowing into the room, the bottom one blowing toward the window. Arrows engraved on both sides of the housing (above the upper duct and below the lower one) show each duct's airflow; match the arrows on the fan frames to them.
  10. Electronics: solder the ESP32 and the TMC2209 onto the perfboard (or into female headers), following the layout: the ESP32 across the back half with its USB-end pins one column in from the board's edge (clear of the corner standoff), the TMC2209 at the front right. Or turn the ESP32 180° in the same place, its USB end toward the right wall (view ebox_elec_alt): the box has a diamond on each side. Wire it (connect the 12 V brick's GND to the ESP32's GND), and screw it onto the four standoffs in the box (M2). Plug the USB-C cable in through the matching side wall. Bring the 12 V wire and the fan and motor cables down the right side, tie them to the three anchors behind the fans, and into the box through the notch at the top of its right wall. The AS5600 wires drop in through the window in the encoder plate.
  11. Fit the box under the housing, its two back tabs up through the notches in the encoder plate: two M3 × 12 through the front cover into the bosses of the bottom bay, and at the back an M3 × 10 through each bay wall and its tab from outside, with a nut on the inside, reached from the bay's open back.
  12. Slide the tube_spacer over both spigots, up against the stop rings. Then push the spigots into the foam holes: the spacer ends up squeezed between the stop rings and the foam. If they're tight, sand the holes a little. (With orings = true, roll two O-rings onto each spigot first.)

Optional: adjustable louvers for the intake fan

In winter the incoming air is cold, and a jet straight off the fan can feel like a draft. These louvers clip onto the room face of the intake fan and aim the air: upward along the ceiling, for example, so it mixes with the room air before it reaches anyone.

Louvers: a frame with six horizontal blades, joined by a link bar on one side, two clips on top and bottom

  • Clips, no screws. Four hooks reach back over the fan's corner flanges and catch behind them. Recesses in the frame's corners fit over the fan's screw heads, so the fan keeps its normal screws. A firm pull takes the louvers off.
  • The blades turn together. Each of the six blades pivots on pins in U-slots in the side walls. On one side, a link bar joins an offset pin on every blade, so turning any blade turns them all, from −45° to +45°. The link runs along the room side of the blades, and a ribbed tab in its middle sticks out of the frame at every angle: push it up or down to set the angle. (Set link_side = "back" to hide the link between the blades and the fan instead, without the tab.)
  • They hold their angle. The pins fit snugly in their slots, and that alone holds the blades: the link bar runs 0.2 mm from the side wall. On the other side, a collar on each blade's pivot bears on the wall. (For more friction or vibration damping, louver_link has a thinner bar for a strip of felt or foam tape that rubs on the wall.)
  • Nothing falls out. The slots open toward the fan, so the blades drop in from behind. A retaining comb in each side wall then plugs the slots behind the pins; a snap arm at each end of the comb clicks it in place, and once the frame is on the fan, the fan's face holds the combs in too.
Part
Thumbnail
QtyDescriptionSize (mm)Print orientation
louver_chassis1Frame: clips onto the fan's corner flanges (recesses clear its screw heads), U-slots for the blade pins in both side walls80 × 86 × 36Front face on the bed, clips up
louver_blade6Blade with a pivot pin at each end, a collar on one side and an offset pin for the link on the other79 × 14 × 3.5Flat; its pins are flattened on the bed side
louver_link_nopad1Link bar joining the six offset pins, so the blades turn together, with the tab that sets the angle; its bar is 3 mm thick (the tab 2.4)53 × 11 × 3Flat, holes and ribs up
louver_link(1)Optional, instead of louver_link_nopad: the same link with a thinner bar (2.4 mm) for a felt strip on its flat face53 × 11 × 2.8Flat, holes and ribs up
louver_retainer2Retaining comb, one per side wall: plugs the slots behind the pins, with a snap arm at each end3 × 57 × 11Spine down, pegs up

About 33 cm³ (42 g) of PETG in all. With the optional louver_link, also a strip of 1 mm felt or foam tape, about 5 × 52 mm.

Assembly:

  1. Only with the optional louver_link: stick the felt strip on the link bar's flat face (the face without holes).
  2. Push the link onto the six blades' offset pins.
  3. Slide the blades into the U-slots from the back of the frame, their offset pins toward the room (the link at the front, its tab pointing out of the frame): the link goes on the side with the link pins, the collars on the other.
  4. Press a retaining comb into the back of each side wall, the small round ridge under each peg down, into the groove in the slot's floor (it keeps the pegs centred in the wall), until both snap arms click. A firm pull takes it out again.
  5. Clip the frame onto the intake fan.

Tuning after a test print, in cad/louvers.scad:

  • slot_w: the grip of the pins in their slots. Lower makes the blades stiffer.
  • clip_lip, clip_edge and clip_w: the clips. They're sized for the Noctua NF-A8 (a 7 mm corner flange, with the clips 8 mm in from the side edges so they clear the corner screws).
  • pad_t and pad_sq: the friction strip and how much it's squeezed.
  • comb_fit, comb_key, key_shape, key_clr and snap_l, snap_t, snap_b: the combs' pegs in their slots, the guide ridge under each peg in its groove (half-round by default, or a V), and the snap arms. Print peg_test2 first (about 25 min): a three-slot slice of the side wall, cut from the chassis itself, with a three-peg comb and its snap arms.

Files

text
doc/                        reference drawings: the motor, the AS5600, ESP32-S3, TMC2209 and SCD40 
                            boards, the electronics box layout, the perfboard build plan (and PDF); 
                            the lip foam and foam insert templates (PDF, to print at 100 %), the 
                            hole saw
images/                     renders used in this README
cad/window_vent.scad        parametric model (Customizer; part = "assembly" for the preview)
cad/louvers.scad            optional adjustable louvers for the intake fan
stl/                        exported parts (default parameters)
pcb/                        the controller PCB (KiCad 10): schematic and its PDF, board, Gerber zip 
                            for ordering, renders
tools/check.py              geometry checks (must print ALL CHECKS PASSED); vent or louvers: just one
tools/wiring_svg.py         draws firmware/images/wiring.svg (the wiring diagram)
tools/as5600_svg.py         draws doc/as5600.svg (the AS5600 board, dimensioned)
tools/ebox_layout_svg.py    draws doc/ebox_layout.svg (the electronics box, to scale)
tools/esp32_svg.py          draws doc/esp32_s3.svg (the ESP32-S3 board, dimensioned)
tools/tmc2209_svg.py        draws doc/tmc2209.svg (the TMC2209 module, dimensioned)
tools/scd40_svg.py          draws doc/scd40.svg (the SCD40 CO2 sensor board, dimensioned)
tools/max9814_svg.py        draws doc/max9814.svg (the MAX9814 microphone board, dimensioned)
tools/motor_svg.py          draws doc/motor_17hs08.svg (the stepper motor, dimensioned)
tools/motor_mount_svg.py    draws doc/motor_mount.svg (section through a motor screw)
tools/lip_templates_svg.py  draws doc/lip_templates.svg and .pdf (full-size foam templates) and 
                            their preview
tools/foam_template_svg.py  draws doc/foam_template.svg and .pdf (drilling template for the foam 
                            insert) and its preview

License

Copyright (c) 2026 Marc-Antoine Massicotte.

This design (CAD model, STL files, images, documentation and scripts) is released under the CERN Open Hardware Licence Version 2 - Permissive (CERN-OHL-P-2.0). You may use, modify, make, share and sell it freely, as long as you keep this notice. It comes with no warranty of any kind, and the author accepts no liability for its use.

The firmware in firmware/ is software, so it is released under the MIT License instead: same idea, free to use, modify and share, keep the notice, no warranty.

Parts

35
QtyValueFootprintReferences
410kResistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_VerticalR1, R2, R3, R4
4M2MountingHole:MountingHole_2.2mm_M2H1, H2, H3, H4
24.7kwindow_vent:R_DIN0207_P10.16mm_Flat_TightR9, R10
10R AUDIOResistor_SMD:R_0402_1005Metric_Pad0.72x0.64mm_HandSolderJP2
10R PDNResistor_SMD:R_0402_1005Metric_Pad0.72x0.64mm_HandSolderJP3
10R UARTResistor_SMD:R_0402_1005Metric_Pad0.72x0.64mm_HandSolderJP4
1100kwindow_vent:R_DIN0207_P10.16mm_Flat_TightR7
110kwindow_vent:R_DIN0207_P10.16mm_Flat_TightR5
112VTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP4
112V INConnector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalJ1
11N5822Diode_THT:D_DO-201AD_P5.08mm_Vertical_AnodeUpD1
11kwindow_vent:R_DIN0207_P10.16mm_Flat_TightR6
1220uF 50VCapacitor_THT:CP_Radial_D10.0mm_P5.00mmC1
122kwindow_vent:R_DIN0207_P10.16mm_Flat_TightR8
13V3TestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP2
1AS5600Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_VerticalJ6
1D1 bypassConnector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalJP1
1DIAGTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP5
1DS18B20 in / MICConnector_PinHeader_2.54mm:PinHeader_2x03_P2.54mm_VerticalJ7
1DS18B20 outConnector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_VerticalJ8
1FAN 0 intakeConnector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_VerticalJ2
1FAN 1 exhaustConnector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_VerticalJ3
1Freenove ESP32-S3 Litewindow_vent:Freenove_ESP32S3_LiteU1
1GNDTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP1
1MOTORConnector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_VerticalJ4
1SCD40Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_VerticalJ5
1TMC2209 modulewindow_vent:TMC2209_StepStickU2
1VINTestPoint:TestPoint_THTPad_D2.0mm_Drill1.0mmTP3

Similar boards

  • 2 layers97.2 × 59.1 mmArmLEDsPrototype, the printed circuit board in 3D

    ArmLEDsPrototype

    hackclub/OnBoard

    💡 Join 1,000 teenagers and make your first PCB with a $100 grant!

    35 parts915
  • 2 layers62 × 42 mmMod cell 0 02, the printed circuit board in 3D

    Mod cell 0 02

    Green-bms/SmartBMS

    Open source Smart Battery Management System

    35 parts756
  • 2 layers75 × 26 mmDIY detector electron variant, the printed circuit board in 3D

    DIY detector electron variant

    ozel/DIY_particle_detector

    A mobile low-cost spectrometer for measuring radioactivity and the energy of ionising radiation like alpha particles and electrons

    35 parts558BSD-2-Clause
  • 2 layers45 × 25 mmReCamera Basically Board, the printed circuit board in 3D

    ReCamera Basically Board

    Seeed-Studio/OSHW-reCamera-Series

    reCamera is a series of open-source AI cameras.

    35 parts495Apache-2.0
  • 2 layers160.8 × 24.4 mmHUD VID RCD PANEL, the printed circuit board in 3D

    HUD VID RCD PANEL

    jrsteensen/OpenHornet

    OpenHornet 1:1 F/A-18C Simulator Repository

    35 parts431
  • 2 layers75 × 50 mmBackplane tester, the printed circuit board in 3D

    Backplane tester

    dennisss/dacha

    Monorepo for awesome tools/projects/libraries

    35 parts397Apache-2.0
  • 2 layers50 × 20.5 mmOtterIron PRO Top, the printed circuit board in 3D

    OtterIron PRO Top

    Jana-Marie/Otter-Iron-PRO

    USB-PD soldering station for JBC C245 handles.

    35 parts321
  • 2 layers50.5 × 67.1 mmHackheld Vega, the printed circuit board in 3D

    Hackheld Vega

    SpacehuhnTech/HackHeld

    Hackable Handheld

    35 parts277

Questions

How big is the Window vent ctrl PCB?

It measures 70 × 50 mm and has 2 copper layers, 1.6 mm thick.

What parts does the Window vent ctrl use?

35 parts. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the Window vent ctrl KiCad files?

From the mmassico/window_air_vent repository on GitHub, at the commit this page was made from: the board is pcb/window_vent_ctrl.kicad_pcb and its schematic pcb/window_vent_ctrl.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the Window vent ctrl design in my own project?

Its repository publishes it under the CERN-OHL-P-2.0 licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).