Skip to content

Custom Enclosure from PCB Data

Custom Enclosure from PCB Data hero image
Modified:
Published:

Automate enclosure design by parsing KiCad PCB files and generating print-ready housings with CadQuery. Your script will extract board outlines, mounting holes, and connector positions, then produce a complete enclosure with standoffs, port cutouts, ventilation, and snap-fit lids that updates whenever the PCB layout changes. #KiCad #EnclosureDesign #PCBAutomation

Learning Objectives

By the end of this lesson, you will be able to:

  1. Parse KiCad .kicad_pcb S-expression files to extract board outlines, mounting holes, and component positions
  2. Generate parametric enclosure shells with controlled wall thickness, corner radii, and tolerances
  3. Place PCB standoffs at mounting hole locations with correct height and screw sizing
  4. Cut port openings for USB, HDMI, Ethernet, GPIO, and power connectors from component data
  5. Design ventilation patterns (slot arrays, hexagonal grids) for thermal management
  6. Implement snap-fit features for tool-free lid attachment
  7. Export a two-part enclosure (shell + lid) as STEP and STL files

Understanding the KiCad PCB File Format



KiCad stores PCB data as S-expressions, nested parenthesized lists similar to Lisp syntax. Every element in the file, from board outlines to component footprints, follows this structure.

S-Expression Structure

A .kicad_pcb file looks like this at the top level:

(kicad_pcb
(version 20211014)
(generator pcbnew)
(general (thickness 1.6))
(layers ...)
(setup ...)
(net 0 "")
(footprint "Connector_USB:USB_C_Receptacle"
(at 82.55 24.13)
...)
(gr_line (start 0 0) (end 85 0) (layer "Edge.Cuts") ...)
(segment ...)
(via ...)
)

The key elements we need to extract are:

Board Outline

Lines and arcs on the Edge.Cuts layer define the PCB boundary. These are gr_line, gr_arc, and gr_circle elements with layer "Edge.Cuts".

Mounting Holes

Footprints named MountingHole* contain pad elements with drill sizes. Their (at x y) fields give positions.

Connector Positions

Footprints for USB, HDMI, Ethernet, etc. contain (at x y rotation) and pad geometry that defines connector extents.

Component Heights

3D model references within footprints (model elements) include Z-offsets and can indicate component heights for clearance.

Parsing KiCad PCB Files with Python



We will build a lightweight S-expression parser rather than relying on external libraries. This keeps the tool self-contained and teaches the parsing fundamentals.

"""
S-expression parser for KiCad .kicad_pcb files.
Converts nested parenthesized text into Python lists and strings.
"""
import re
from typing import Union
# Type alias for parsed S-expression tree
SExpr = Union[str, list]
def tokenize(text: str) -> list[str]:
"""
Break S-expression text into tokens: '(', ')', quoted strings, and atoms.
"""
token_pattern = r'''
\( | # open paren
\) | # close paren
"[^"]*" | # quoted string (preserves spaces)
[^\s()"]+ # unquoted atom (numbers, keywords)
'''
return re.findall(token_pattern, text, re.VERBOSE)
def parse_sexpr(text: str) -> SExpr:
"""
Parse an S-expression string into nested Python lists.
Example:
'(footprint "USB_C" (at 82.55 24.13))'
→ ['footprint', 'USB_C', ['at', '82.55', '24.13']]
"""
tokens = tokenize(text)
pos = [0] # mutable index for recursive descent
def parse_one() -> SExpr:
if pos[0] >= len(tokens):
raise ValueError("Unexpected end of input")
token = tokens[pos[0]]
if token == '(':
pos[0] += 1
result = []
while pos[0] < len(tokens) and tokens[pos[0]] != ')':
result.append(parse_one())
if pos[0] >= len(tokens):
raise ValueError("Missing closing parenthesis")
pos[0] += 1 # skip ')'
return result
elif token == ')':
raise ValueError("Unexpected closing parenthesis")
else:
pos[0] += 1
# Strip quotes from quoted strings
if token.startswith('"') and token.endswith('"'):
return token[1:-1]
return token
return parse_one()

Enclosure Design Parameters



Before generating geometry, we define all enclosure parameters in one place. This makes the design fully parametric: any parameter change propagates through the entire model.

Shell Parameters

  • Wall thickness: 2.0 mm (minimum for FDM strength)
  • Corner radius: 3.0 mm (exterior, interior = exterior - wall)
  • Bottom thickness: 2.0 mm
  • PCB clearance below: 3.0 mm (for bottom components / standoffs)
  • PCB clearance above: 15.0 mm (tallest component + margin)

Tolerance & Fit

  • PCB tolerance: 0.5 mm per side (thermal expansion + insertion clearance)
  • Connector tolerance: 0.3 mm per side (looser for plug insertion)
  • Snap-fit interference: 0.3 mm (for PLA/PETG at room temperature)
  • Lid gap: 0.15 mm per side (for smooth sliding fit)

Standoff Dimensions

  • Standoff diameter: 6.0 mm (outer)
  • Standoff hole diameter: 2.2 mm (for M2.5 self-tapping screw or heat-set insert)
  • Standoff height: 3.0 mm (PCB clearance below)
  • Boss chamfer: 0.5 mm (print quality improvement)

Ventilation

  • Slot width: 1.5 mm
  • Slot length: 12.0 mm
  • Slot spacing: 3.0 mm (center-to-center)
  • Edge margin: 5.0 mm (structural integrity around vents)
"""
Enclosure design parameters — single source of truth.
All dimensions in millimeters.
"""
from dataclasses import dataclass
@dataclass
class EnclosureParams:
# Shell geometry
wall_thickness: float = 2.0
corner_radius: float = 3.0
bottom_thickness: float = 2.0
# PCB clearances
pcb_clearance_below: float = 3.0 # space under PCB (standoff height)
pcb_clearance_above: float = 15.0 # space above PCB top surface
# Tolerances
pcb_tolerance: float = 0.5 # gap between PCB edge and inner wall
connector_tolerance: float = 0.3 # extra clearance around port cutouts
lid_tolerance: float = 0.15 # gap for lid fit
# Standoffs
standoff_outer_dia: float = 6.0
standoff_hole_dia: float = 2.2 # M2.5 self-tapping or heat-set
standoff_chamfer: float = 0.5
# Snap-fit
snap_width: float = 8.0
snap_depth: float = 1.5 # how far the hook protrudes
snap_height: float = 2.0 # height of the catch
snap_interference: float = 0.3 # press-fit interference
# Ventilation slots
vent_slot_width: float = 1.5
vent_slot_length: float = 12.0
vent_slot_spacing: float = 3.0
vent_margin: float = 5.0
@property
def interior_height(self) -> float:
"""Total interior height = below PCB + board + above PCB."""
return self.pcb_clearance_below + 1.6 + self.pcb_clearance_above
@property
def exterior_height(self) -> float:
"""Total box height = interior + bottom + top wall."""
return self.interior_height + self.bottom_thickness + self.wall_thickness
def interior_width(self, board: 'BoardData') -> float:
return board.width + 2 * self.pcb_tolerance
def interior_depth(self, board: 'BoardData') -> float:
return board.height + 2 * self.pcb_tolerance

CadQuery Enclosure Generation



Now we build the enclosure step by step. Each function handles one feature, and they compose together to produce the final shell and lid.

"""
Generate the main enclosure shell — a hollow box with rounded corners.
"""
import cadquery as cq
def make_shell_body(board: BoardData, params: EnclosureParams) -> cq.Workplane:
"""
Create the outer shell as a rounded box, then hollow it out.
The shell is oriented with:
- X along board width (85mm for RPi)
- Y along board depth (56mm for RPi)
- Z upward (bottom of enclosure at Z=0)
"""
# Interior dimensions
int_w = params.interior_width(board)
int_d = params.interior_depth(board)
int_h = params.interior_height
# Exterior dimensions
ext_w = int_w + 2 * params.wall_thickness
ext_d = int_d + 2 * params.wall_thickness
ext_h = int_h + params.bottom_thickness # open top for now
# Outer box with rounded vertical edges
outer = (
cq.Workplane("XY")
.box(ext_w, ext_d, ext_h, centered=(True, True, False))
.edges("|Z") # select vertical edges
.fillet(params.corner_radius)
)
# Inner cavity (cut from top)
inner = (
cq.Workplane("XY")
.workplane(offset=params.bottom_thickness)
.rect(int_w, int_d)
.extrude(int_h + 1) # +1 to ensure clean cut through top
)
# Apply corner radius to interior
inner_radius = max(params.corner_radius - params.wall_thickness, 0.5)
inner = inner.edges("|Z").fillet(inner_radius)
shell = outer.cut(inner)
return shell
def add_shell_lip(shell: cq.Workplane, board: BoardData,
params: EnclosureParams) -> cq.Workplane:
"""
Add a stepped lip around the top of the shell for lid alignment.
The lip is a thin ledge recessed inward by half the wall thickness.
"""
int_w = params.interior_width(board)
int_d = params.interior_depth(board)
lip_width = params.wall_thickness / 2 - params.lid_tolerance
lip_height = 2.0 # mm, how tall the lip step is
# The lip sits at the top of the shell, inset from the outer wall
lip_offset = params.bottom_thickness + params.interior_height - lip_height
lip = (
cq.Workplane("XY")
.workplane(offset=lip_offset)
.rect(int_w + lip_width * 2, int_d + lip_width * 2)
.rect(int_w, int_d) # inner cutout
.extrude(lip_height)
)
shell = shell.union(lip)
return shell

Lid Design



The lid completes the enclosure. It includes alignment features to register with the shell lip and an optional GPIO slot for the Raspberry Pi header.

"""
Lid generation with alignment lip and optional GPIO slot.
"""
import cadquery as cq
def make_lid(board: BoardData, params: EnclosureParams) -> cq.Workplane:
"""
Create the enclosure lid with:
- Flat top panel
- Alignment lip that nests inside the shell walls
- Rounded corners matching the shell
"""
int_w = params.interior_width(board)
int_d = params.interior_depth(board)
# Outer dimensions match shell exterior
ext_w = int_w + 2 * params.wall_thickness
ext_d = int_d + 2 * params.wall_thickness
lid_thickness = params.wall_thickness # same as walls
lip_height = 2.0 # must match shell lip
lip_inset = params.wall_thickness / 2 + params.lid_tolerance
# Main lid panel
lid = (
cq.Workplane("XY")
.box(ext_w, ext_d, lid_thickness, centered=(True, True, False))
.edges("|Z")
.fillet(params.corner_radius)
)
# Alignment lip hanging down from lid interior
lip_w = ext_w - 2 * lip_inset
lip_d = ext_d - 2 * lip_inset
inner_lip_w = lip_w - 2 * params.wall_thickness / 2
inner_lip_d = lip_d - 2 * params.wall_thickness / 2
lip = (
cq.Workplane("XY")
.workplane(offset=-lip_height)
.rect(lip_w, lip_d)
.rect(inner_lip_w, inner_lip_d)
.extrude(lip_height)
)
lip_radius = max(params.corner_radius - lip_inset, 0.5)
lip = lip.edges("|Z").fillet(lip_radius)
lid = lid.union(lip)
return lid
def add_gpio_slot_to_lid(lid: cq.Workplane, board: BoardData,
params: EnclosureParams) -> cq.Workplane:
"""
Cut a slot in the lid for GPIO header access.
Only applies if a GPIO connector exists in the board data.
"""
int_w = params.interior_width(board)
int_d = params.interior_depth(board)
origin_x = -int_w / 2 + params.pcb_tolerance
origin_y = -int_d / 2 + params.pcb_tolerance
tol = params.connector_tolerance
for conn in board.connectors:
if conn.connector_type != "GPIO":
continue
cx = origin_x + conn.position.x
cy = origin_y + conn.position.y
# GPIO slot dimensions
slot_w = conn.width + 2 * tol
slot_d = conn.depth + 2 * tol
slot = (
cq.Workplane("XY")
.workplane(offset=-1)
.center(cx, cy)
.rect(slot_w, slot_d)
.extrude(params.wall_thickness + 2)
)
lid = lid.cut(slot)
return lid
def add_lid_label(lid: cq.Workplane, text: str = "RPi4",
depth: float = 0.4) -> cq.Workplane:
"""
Emboss or engrave a text label on the lid top surface.
Note: CadQuery text requires the 'text' parameter as a string.
Engraved text is easier to print reliably than embossed.
"""
label = (
cq.Workplane("XY")
.workplane(offset=0) # top surface of lid
.text(text, fontsize=8, distance=-depth)
)
lid = lid.cut(label)
return lid

Complete Assembly: Raspberry Pi 4 Enclosure



Now we bring all the pieces together. This is the main script that generates the complete enclosure from Raspberry Pi 4 board data.

"""
Complete Raspberry Pi 4 enclosure generation.
Run this script to produce shell.step, lid.step, shell.stl, lid.stl.
"""
import cadquery as cq
# --- Configuration ---
board = raspberry_pi_4_data()
params = EnclosureParams(
wall_thickness=2.0,
corner_radius=3.0,
bottom_thickness=2.0,
pcb_clearance_below=3.0,
pcb_clearance_above=15.0,
pcb_tolerance=0.5,
connector_tolerance=0.3,
)
# --- Shell Generation ---
print("Building shell body...")
shell = make_shell_body(board, params)
print("Adding shell lip...")
shell = add_shell_lip(shell, board, params)
print("Adding PCB standoffs...")
shell = add_standoffs(shell, board, params)
print("Cutting port openings...")
shell = add_port_cutouts(shell, board, params)
print("Adding ventilation (hex pattern on bottom)...")
shell = add_hex_ventilation(shell, board, params, hex_radius=2.5, hex_spacing=1.5)
print("Adding side ventilation slots...")
shell = add_ventilation_slots(shell, board, params, face="left")
shell = add_ventilation_slots(shell, board, params, face="right")
print("Adding snap-fit hooks...")
shell = add_snap_hooks(shell, board, params)
# --- Lid Generation ---
print("Building lid...")
lid = make_lid(board, params)
print("Cutting GPIO slot...")
lid = add_gpio_slot_to_lid(lid, board, params)
print("Engraving label...")
lid = add_lid_label(lid, text="RPi 4B")
# --- Export ---
print("Exporting STEP files...")
cq.exporters.export(shell, "rpi4_shell.step")
cq.exporters.export(lid, "rpi4_lid.step")
print("Exporting STL files (for 3D printing)...")
cq.exporters.export(shell, "rpi4_shell.stl")
cq.exporters.export(lid, "rpi4_lid.stl")
# --- Print summary ---
ext_w = params.interior_width(board) + 2 * params.wall_thickness
ext_d = params.interior_depth(board) + 2 * params.wall_thickness
ext_h = params.exterior_height
print(f"\nEnclosure dimensions (exterior):")
print(f" Width: {ext_w:.1f} mm")
print(f" Depth: {ext_d:.1f} mm")
print(f" Height: {ext_h:.1f} mm")
print(f"\nBoard: {board.width} x {board.height} mm")
print(f"Mounting holes: {len(board.mounting_holes)}")
print(f"Port cutouts: {len(board.connectors)}")
print(f"\nFiles exported:")
print(f" rpi4_shell.step — CNC/CAD interchange")
print(f" rpi4_lid.step — CNC/CAD interchange")
print(f" rpi4_shell.stl — 3D printing")
print(f" rpi4_lid.stl — 3D printing")

Expected Output

Building shell body...
Adding shell lip...
Adding PCB standoffs...
Cutting port openings...
Adding ventilation (hex pattern on bottom)...
Adding side ventilation slots...
Adding snap-fit hooks...
Building lid...
Cutting GPIO slot...
Engraving label...
Exporting STEP files...
Exporting STL files (for 3D printing)...
Enclosure dimensions (exterior):
Width: 90.0 mm
Depth: 61.0 mm
Height: 23.6 mm
Board: 85.0 x 56.0 mm
Mounting holes: 4
Port cutouts: 8
Files exported:
rpi4_shell.step — CNC/CAD interchange
rpi4_lid.step — CNC/CAD interchange
rpi4_shell.stl — 3D printing
rpi4_lid.stl — 3D printing

Design for 3D Printing



Enclosures are almost always 3D-printed during prototyping. Here are critical considerations for printability.

Shell Orientation

Print the shell upside down (open side facing up):

  • No support material needed for the cavity
  • Bottom face gets smooth bed adhesion surface
  • Ventilation holes print cleanly (no bridging for small hex holes)
  • Standoffs print as solid cylinders (strong)

Lid Orientation

Print the lid top face down:

  • Flat, smooth top surface from bed contact
  • Alignment lip prints upward (no supports)
  • GPIO slot is a simple through-hole (clean print)

Extending the Design



Custom Boards

Replace raspberry_pi_4_data() with parse_kicad_pcb("your_board.kicad_pcb") to generate enclosures for any KiCad PCB. The parser extracts all geometry automatically.

Screw Bosses

Replace snap-fits with screw bosses by adding threaded inserts at the shell corners. Heat-set inserts (M2.5 or M3) provide the strongest joint for production enclosures.

Light Pipes

Add conical light pipes from LED positions on the PCB to the lid surface. Parse LED footprint positions from the PCB data and generate matching geometry.

EMI Shielding

For RF applications, add a conductive gasket groove around the lid perimeter. The groove geometry is a simple rectangular channel milled into the shell lip face.

What You Have Learned



In this lesson, you:

  • Parsed KiCad .kicad_pcb S-expression files to extract board outlines, mounting holes, and connector positions using a custom Python parser
  • Generated parametric enclosure shells with controlled wall thickness, corner radii, and tolerances in CadQuery
  • Placed PCB standoffs at precise mounting hole locations with chamfers and gussets for rigidity
  • Cut port openings for USB-C, micro-HDMI, USB-A, Ethernet, audio, and GPIO connectors
  • Designed both slot-array and hexagonal ventilation patterns for thermal management
  • Implemented cantilever snap-fit hooks with engineering calculations for beam length and allowable strain
  • Built a complete two-part enclosure (shell + lid) and exported it as STEP and STL files
  • Applied 3D printing design guidelines including orientation, material selection, and tolerance calibration

Next up: Heat Sink Design & Thermal Optimization. Model thermal resistance from junction to ambient, compare fin geometries, and optimize heat sink designs with parameter sweeps.

Comments

Loading comments...


© 2021-2026 SiliconWit®. All rights reserved.