FreeCAD Mk2 Macro — dilder_rev2_mk2.FCMacro¶
The Mk2 macro is the parametric source of truth for the Rev 2 enclosure. One Python file builds three PartDesign::Body objects (BasePlate, AAACradle, TopCover), positions them as an assembly, imports the Raspberry Pi Pico 2 W board STEP file, generates pin headers procedurally, and saves the result to Dilder_Rev2_Mk2.FCStd.
This page documents how the macro is structured and how to drive it.
File layout¶
hardware-design/
├── freecad-mk2/
│ ├── dilder_rev2_mk2.FCMacro ← the macro
│ ├── Dilder_Rev2_Mk2.FCStd ← output document
│ ├── Dilder_Rev2_Mk2-*.3mf ← exported meshes
│ └── BUILD-INSTRUCTIONS.txt
└── reference-boards/
└── raspberry-pi-pico-2/
├── RaspberryPi-Pico-2.step ← 1.7 MB official STEP
└── Pico-2-step.zip
Running the macro¶
Or open FreeCAD's GUI and run via Macro → Macros... → Execute. After it finishes:
- The active document is
Dilder_Rev2_Mk2 - The file is saved alongside the macro
- Console prints feature counts per body and the Pico's final bbox
Architecture¶
The macro splits cleanly into five sections:
| Section | Function | Output |
|---|---|---|
| 1. Helpers | find_edges, edges_at_z, make_rect_sketch, make_circle_sketch, SP |
Reusable utilities |
| 2. Spreadsheet | setup_spreadsheet |
Parameters sheet with 100+ aliased cells |
| 3. Bodies | build_base_plate, build_cradle, build_top_cover |
Three PartDesign Bodies |
| 4. Pico import | add_pico_with_headers |
Pico2W_Board + Pico2W_Headers features |
| 5. Peripherals | add_peripherals |
AAA_Battery_1/2, TP4056_Module, EInkDisplay_Module |
| 6. Main | main |
Spreadsheet → bodies → assembly placements → Pico import → peripherals → save |
The Parameters spreadsheet¶
Every dimension that anyone is likely to want to change lives in a single FreeCAD spreadsheet (Parameters) with named aliases. Sketches and primitives bind to these via setExpression(...) calls. The SP("alias") helper just produces the string "Parameters.alias".
To change the enclosure width, edit Parameters.enc_x and recompute (Ctrl+Shift+R). Every dependent feature updates.
Sketch helpers¶
Two helpers cover most plan-view sketches:
make_rect_sketch(body, name, z_offset, x0, y0, x1, y1)— a fully constrained rectangle on the XY plane atz_offset, returning the sketch and the four constraint indices (c_ox, c_oy, c_w, c_h) so the caller can bind them to spreadsheet expressions.make_circle_sketch(body, name, z_offset, cx, cy, r)— same but for circles, returning the three constraints(c_x, c_y, c_r).
For features like USB-C cutouts where a sketch on a vertical face would require fighting the face-coordinate system, the macro uses PartDesign::SubtractiveBox and PartDesign::SubtractiveCylinder with explicit XYZ placements instead. These are simpler and just as parametric (Length, Width, Height, Radius accept expressions).
Edge finders¶
PartDesign Fillets need an edge list. Edge indices change as the model rebuilds, so the macro re-discovers them by orientation:
find_edges(feature, axis="vertical") # corner edges for rounding
find_edges(feature, axis="horizontal_bottom") # bottom rim for chamfer
edges_at_z(feature, z_target=8.0) # peg-tip rim for chamfer
Body builds¶
build_base_plate(doc)¶
Step-by-step:
- Outer profile sketch (
Sk_BP_Outline), pinned to origin - Pad → 6 mm box (
bp_hpost-shave) - Fillet vertical edges (
corner_r) - Fillet bottom edges (
bp_fillet) - Subtract the cradle pocket (top-down)
- Subtract the solar pit (bottom-up)
- USB-C stadium cutout → 3 subtractive primitives (rectangular middle, bottom strip, two corner cylinders)
- Four corner pillars + extension wings + pegs + tip chamfers
- Two battery rail troughs (additive box + cylinder cut)
- Two USB support blocks
- Pico retention block
- Two solar wire holes (last so they cut through every additive feature added above)
The wire-hole ordering was a real bug in the first version — they were added at step 7 and only cut through what existed at that moment. Moving them to the end fixed it.
build_cradle(doc)¶
The AAA battery cradle inserts into the base plate's pocket. Built features:
- Plug body sketch + pad (
plug_thick = bay_d + 2 * aaa_bwall = 12.1 mm) - Four pillar cutouts (matching the base plate's corner pillars)
- FPC ribbon gap on the −X side
- Two AAA bays (cylinder along X + top-open slot)
- Pico nest (large rectangular subtraction across the bottom of the plug)
- −X inset (clears space for the connecting block)
- +X display connector cutout
- Connecting block (re-fills the −X inset, with battery arcs cut through it)
- TP4056 indent on the connecting block top
- Four battery clip slots + four retainer windows
Face44 of the cumulative cradle solid (the +X-facing inner wall of the PicoNest cavity, in cradle local coordinates) becomes the X-anchor for the Pico import. Face6 (the +Z mating face of the plug top in local coordinates) becomes the Z-anchor.
build_top_cover(doc)¶
The top cover is an inverted shell that snaps over the cradle. Features:
- Outer shell sketch + pad
- Fillet vertical edges (corner radius)
- Fillet top edges (bullnose)
- Interior cavity subtraction
- Four corner pillars (fill the cavity corners to seat against the cradle)
- Four M3 screw bores
- Screen inlay pocket
- FPC ribbon divet
- Display viewing window (rectangular through-cut)
- Joystick through-hole (cylinder)
- Joystick PCB pocket (subtractive box from below)
Assembly placement¶
After all three bodies exist, main() positions them in world coordinates:
bp_top = 6.0 # base plate top Z (post-shave)
# Top cover — bottom face flush with base plate top
tc.Placement = App.Placement(V(0, 0, bp_top + 5.0), ROT0)
# Cradle — flipped 180° about Y, then translated so the plug-top face
# (formerly local Z=7.0) lands flush with bp_top
cr.Placement = App.Placement(
V(96.3, 0, bp_top + 7.0),
App.Rotation(V(0, 1, 0), 180))
doc.recompute()
The 180° Y rotation is what flips the cradle so its PicoNest cavity opens downward toward the base plate. Without it, the cradle would mount upside-down relative to the rest of the assembly.
Pico 2 W import¶
add_pico_with_headers(doc) runs after the placements above. Five phases:
1. Read & orient¶
shape = Part.Shape()
shape.read(step_path)
# Bbox-driven rotation: shortest axis → +Z
bb0 = shape.BoundBox
short = [bb0.XLength, bb0.YLength, bb0.ZLength].index(min(...))
m = App.Matrix()
if short == 0: m.rotateY(math.radians(90))
elif short == 1: m.rotateX(math.radians(90))
shape = shape.transformGeometry(m)
# If long axis ended up on Y, swing it onto X
if shape.BoundBox.YLength > shape.BoundBox.XLength:
m_z = App.Matrix(); m_z.rotateZ(math.radians(90))
shape = shape.transformGeometry(m_z)
After this the board is flat in XY, components on +Z, long axis on X. The macro is robust to any source-CAD orientation.
2. Optional X-reverse¶
PICO_USB_PLUS_X = False flips the board 180° about Z so the USB-C end sits on the −X side of the board's bbox. This matches the Rev 2 design intent (USB-C faces the cradle's connecting block area, not the +X wall).
3. Procedural headers¶
2×20 pin headers built below the board. Plastic shroud (2.54 × 50.8 × 2.54 mm) sits on the board's bottom face, pins (0.64 × 0.64 × 8.6 mm) extend further below.
4. Z-flip¶
m_flip_z = App.Matrix(); m_flip_z.rotateX(math.radians(180))
shape = shape.transformGeometry(m_flip_z)
header_shape = header_shape.transformGeometry(m_flip_z)
Both shapes rotate together. After the flip, headers are above the board, components face the base plate.
5. Anchor to named faces¶
resolve_face_global(spec) looks up the face on the cradle's Tip shape, applies the cradle's Placement to its center of mass, and returns the global point.
X anchor → translate so bbox.XMin lands on Face44's global X.
Y anchor → center the bbox at enc_y / 2.
Z anchor → translate so bbox.ZMax − PCB_THICKNESS (the post-flip component-side face) lands on Face6's global Z.
The result:
| Location | Z |
|---|---|
| Component tips (bbox ZMin) | 3.27 |
| PCB component face (anchored) | 6.00 |
| Back of PCB (bbox ZMax) | 7.00 |
| Header shroud top | 9.54 |
| Pin tips | 18.14 |
Peripherals¶
add_peripherals(doc) runs after the Pico import and adds three more components, all built procedurally from datasheet dimensions:
AAA batteries¶
Two cylinders (10.5 mm dia × 39.5 mm long, dark grey) plus a smaller positive-terminal cap on the +X end (5.5 mm dia × 1 mm tall, brass color). Positioned in the cradle's two battery bays at global Y = 7.85 / 38.15, Z = 12.05 (= 13 − bay_cz). Cylinder axes lie along X with the cell centered in the bay's effective length range.
TP4056 USB-C charge module¶
A 28 × 17 × 1.6 mm navy blue PCB with a small IC + capacitor block on top and a silver USB-C connector protruding +X (toward the base plate's +X-wall USB-C cutout). Sits in the cradle's TP4056_Indent recess so the board's top face is flush with the cradle's plug-top mating plane at global Z = 6.0. Components hang down toward the base plate.
Waveshare 2.13" e-paper display¶
Off-white module body (65 × 30 × 3 mm) sized to fill the top cover's screen inlay, with a brighter "paper white" panel rectangle (50 × 22 mm, win-shifted toward +X) on the +Z face representing the active e-ink area. Module back sits flush with the inlay floor at global Z = 18.0; active panel surface at Z = 21.0, visible through the cover's display window cut.
Why procedural instead of imported STEP? Most third-party CAD models for these (TP4056, Waveshare e-paper) are behind login or paywalls (CraftedTech, GrabCAD), and Waveshare doesn't publish CAD for the Pico-ePaper-2.13. Procedural geometry stays fast, deterministic, and parametric — same pattern as the Pico's pin headers.
Tweaking it¶
| Goal | Change |
|---|---|
| Different Pico orientation | Flip PICO_USB_PLUS_X |
| Different anchor face | Edit PICO_ANCHOR_FACE / PICO_Z_ANCHOR_FACE tuples |
| Different board (e.g. another 51×21 mm RP2040 clone) | Swap the STEP file path; the bbox-orient code handles re-orientation |
| Different pin header count / pitch | Edit pin_count, pin_pitch, pin_above, shroud_h |
| Add a TP4056 board | Mirror add_pico_with_headers with a new STEP and different anchor face |
Limitations¶
- Face indices are not stable across feature reorders. If a new feature is inserted into the cradle build before
ClipWindow_2P, Face44 may shift. A future improvement is to look up faces by topological signature (planar face with normal(−1, 0, 0)and bbox center near a specific point) instead of by index. - The official STEP doesn't include the wireless module — the Pico 2 W antenna lump on top isn't represented. For mechanical fitment this doesn't matter (the cradle's PicoNest has plenty of vertical clearance).
- Assembly is positional, not constrained. FreeCAD's PartDesign workbench doesn't ship with assembly mates. Moving a body requires editing its
Placementdirectly. The Assembly4 workbench would solve this but adds a heavy dependency.
Output¶
After running, you should see the four render angles documented on the front page:
assembly-with-pico-iso.png— translucent cover, full stack visibleassembly-with-pico-no-cover.png— top cover hidden, cradle and Pico visibleassembly-with-pico-on-base.png— cradle hidden, Pico's seating exposedassembly-with-pico-front.png— front elevation through the USB-C-cutout side
For the build narrative behind this macro, see the blog post Pico 2 W in the FreeCAD Assembly.