// ===================================================================== // Raspberry Pi Zero (W / 2 W) + Waveshare "RPi Relay Board" (3-ch HAT) // Two-part enclosure: base tray + flat screw-down lid. // Printer: Bambu Lab P1-series, PLA, 0.4 nozzle, 0.2 mm layers. // // COORDINATE FRAME ("hole frame", same as the KiCad reference board): // origin = mounting hole on the GPIO-header edge, at the CSI-camera end // +X = along the long edge, toward the micro-SD end (pin 1 end) // +Y = away from the GPIO header, toward the relay screw terminals // Z = 0 = top surface of the case floor // // Choose what to render with `part`, then F6 + export STL. // ===================================================================== part = "assembly"; // [assembly, base, lid, spacer, cable_plug, print_plate, kicad_check] $fn = 56; // ---------------- Board data (checked against KiCad, see kicad/) ------- pi_w = 65; pi_d = 30; pi_t = 1.4; // Pi Zero PCB hat_w = 65.15; hat_d = 56.21; hat_t = 1.6; // Waveshare RPi Relay Board board_r = 3; // corner radius, both boards hole_dx = 58; // hole spacing along X (both boards) pi_hole_dy = 23; // Pi Zero hole spacing along Y hat_hole_dy = 49; // HAT hole spacing along Y pi_hole_inset = 3.5; // Pi: hole centre to edge (all sides) hat_hole_inset_x0 = 3.63; // HAT: hole to CSI-end edge (Waveshare drawing) hat_hole_inset_y0 = 3.5; // HAT: hole to header edge // Board extents in the hole frame pi_x0 = -pi_hole_inset; pi_y0 = -pi_hole_inset; hat_x0 = -hat_hole_inset_x0; hat_y0 = -hat_hole_inset_y0; // Pi Zero connector centres in the hole frame (ports on the far Y edge) pi_port_y = pi_y0 + pi_d; // = 26.5, edge with mini-HDMI / USB usb_pwr_x = 7.5; // micro-USB PWR IN usb_otg_x = 20.1; // micro-USB data (OTG) hdmi_x = 49.1; // mini-HDMI (not brought out of the case) sd_y = 9.6; // micro-SD, on the +X short edge csi_y = 11.5; // camera connector, on the -X short edge // Relay board features (from the Waveshare dimension drawing) // Screw terminal blocks: X ranges in the hole frame, on the far Y edge term_x = [[3.5, 20.1], [21.3, 37.3], [38.5, 55.0]]; term_h = 10; // terminal block height above HAT term_screw_y = hat_y0 + hat_d - 8; // centre line of the terminal screws (~44.7) front_wire_style = "windows"; // [windows, channel] 3 windows, or one long slot lid_screw_slots = true; // screwdriver slots in the lid over the terminal screws screw_slot_w = 6; // slot width (Y); a 3 mm flat blade fits easily // Indicator LEDs [x, y, w, h] for lid windows (verify on your board) leds = [[20.1, 10.5, 4, 9], [33.7, 10.5, 4, 9], [47.4, 10.5, 4, 9], [53.5, 13.3, 4, 6]]; led_names = ["CH1", "CH2", "CH3", "PWR"]; // ---------------- Stack heights --------------------------------------- pi_standoff = 4; // floor -> underside of Pi stack_gap = 11; // Pi top -> HAT underside (male + female header = 11 mm) relay_h = 15.5; // tallest part on HAT (SRD relays) above HAT top top_clear = 2; // air gap under lid // ---------------- Case parameters ------------------------------------- clear_xy = 3; // board edge -> inner wall wall = 2.4; // 6 perimeters @ 0.4 floor_t = 2; lid_t = 2.4; outer_r = 4; fit = 0.25; // lid lip clearance m25_pilot = 2.2; // M2.5 self-tapping into PLA m3_pilot = 2.6; // M3 self-tapping into PLA m3_clear = 3.4; countersink = true; // lid holes for M3 countersunk screws // 40-pin ribbon breakout plugged onto the relay board's pass-through header. // The ribbon leaves through an open-top notch in the header-side (-Y) wall, // so it can be dropped in with the IDC plug already fitted; the lid closes it. ribbon = true; idc_h = 9.5; // IDC socket height above the HAT pins' base (HAT top) ribbon_w = 56; // 40-way IDC body is ~55.9 mm; ribbon itself ~51 mm ribbon_slot_h = 4; // notch depth below the lid (ribbon is ~1.3 mm thick) usb_window = [13, 9]; // W x H opening, big enough to pass a micro-USB plug usb_otg_hole = false; // second opening for the data USB port sd_slot = true; csi_slot = false; vents = true; led_windows = true; lid_labels = true; ears = true; // screw-down mounting tabs ear_hole = 4.5; // ---------------- Derived --------------------------------------------- pi_z0 = pi_standoff; pi_top = pi_z0 + pi_t; hat_z0 = pi_top + stack_gap; hat_top = hat_z0 + hat_t; inner_h = hat_top + max(relay_h, ribbon ? idc_h + 3 : 0) + top_clear; header_cx = 29; // header centre X (pins 4.87 .. 53.13) in_x0 = min(pi_x0, hat_x0) - clear_xy; in_x1 = max(pi_x0 + pi_w, hat_x0 + hat_w) + clear_xy; in_y0 = min(pi_y0, hat_y0) - clear_xy; in_y1 = max(pi_y0 + pi_d, hat_y0 + hat_d) + clear_xy; in_w = in_x1 - in_x0; in_d = in_y1 - in_y0; out_x0 = in_x0 - wall; out_y0 = in_y0 - wall; out_w = in_w + 2*wall; out_d = in_d + 2*wall; base_h = floor_t + inner_h; // outer height of the base post_r = 3; post_off = 1; // post centre offset from inner corner (diagonal) posts = [[in_x0 + post_off, in_y0 + post_off], [in_x1 - post_off, in_y0 + post_off], [in_x0 + post_off, in_y1 - post_off], [in_x1 - post_off, in_y1 - post_off]]; pi_holes = [[0,0], [hole_dx,0], [0,pi_hole_dy], [hole_dx,pi_hole_dy]]; hat_holes = [[0,0], [hole_dx,0], [0,hat_hole_dy], [hole_dx,hat_hole_dy]]; echo(str("Outer size ", out_w, " x ", out_d, " x ", base_h + lid_t, " mm")); echo(str("Inner size ", in_w, " x ", in_d, " x ", inner_h, " mm")); echo(str("HAT top at z=", hat_top, " relay top at z=", hat_top + relay_h)); // ---------------- Helpers --------------------------------------------- module rrect(x0, y0, w, d, r) { translate([x0 + r, y0 + r]) offset(r = r) square([w - 2*r, d - 2*r]); } module rbox(x0, y0, w, d, h, r) { linear_extrude(h) rrect(x0, y0, w, d, r); } // ---------------- Base ------------------------------------------------ module base() { difference() { union() { // shell difference() { translate([0,0,-floor_t]) rbox(out_x0, out_y0, out_w, out_d, base_h, outer_r); rbox(in_x0, in_y0, in_w, in_d, inner_h + 1, max(0.5, outer_r - wall)); } // corner posts for the lid screws for (p = posts) translate([p[0], p[1], 0]) cylinder(r = post_r, h = inner_h); // Pi bosses (header-side pair also carries the HAT via spacers) for (h = pi_holes) translate([h[0], h[1], 0]) cylinder(d = 6, h = pi_standoff); // tall HAT bosses at the overhanging (terminal) side for (h = [hat_holes[2], hat_holes[3]]) translate([h[0], h[1], 0]) { cylinder(d1 = 9, d2 = 6, h = 4); cylinder(d = 6, h = hat_z0); } if (ears) for (s = [0, 1]) ear(s); } // screw pilots for (p = posts) translate([p[0], p[1], inner_h - 12]) cylinder(d = m3_pilot, h = 13); for (h = pi_holes) translate([h[0], h[1], -1.5]) cylinder(d = m25_pilot, h = pi_standoff + 2); for (h = [hat_holes[2], hat_holes[3]]) translate([h[0], h[1], hat_z0 - 8]) cylinder(d = m25_pilot, h = 9); // --- wall openings --- usb_cz = pi_top + 1.3; // micro-USB centre height // PWR micro-USB: window in the +Y wall (pass the plug through, then fit cable_plug) translate([usb_pwr_x - usb_window[0]/2, in_y1 - 1, usb_cz - usb_window[1]/2]) cube([usb_window[0], wall + 2, usb_window[1]]); if (usb_otg_hole) translate([usb_otg_x - usb_window[0]/2, in_y1 - 1, usb_cz - usb_window[1]/2]) cube([usb_window[0], wall + 2, usb_window[1]]); // relay terminal wire windows (+Y wall), one per channel if (front_wire_style == "channel") translate([term_x[0][0] + 0.5, in_y1 - 1, hat_top + 0.5]) cube([term_x[2][1] - term_x[0][0] - 1, wall + 2, term_h + 0.5]); else for (t = term_x) translate([t[0] + 0.5, in_y1 - 1, hat_top + 0.5]) cube([t[1] - t[0] - 1, wall + 2, term_h + 0.5]); // ribbon cable exit: open-top notch in the -Y (header-side) wall if (ribbon) translate([header_cx - ribbon_w/2, out_y0 - 1, inner_h - ribbon_slot_h]) cube([ribbon_w, wall + 2, ribbon_slot_h + 1]); // micro-SD access (+X wall) if (sd_slot) translate([in_x1 - 1, sd_y - 7, pi_z0 - 1.5]) cube([wall + 2, 14, 6.5]); // camera ribbon (-X wall) if (csi_slot) translate([out_x0 - 1, csi_y - 9, pi_top]) cube([wall + 2, 18, 2.5]); // vents on both short walls, above the HAT if (vents) for (x = [out_x0 - 1, in_x1 - 1]) for (y = [in_y0 + 14 : 4.5 : in_y1 - 12]) translate([x, y, hat_top + 4]) cube([wall + 2, 2, 9]); } } module ear(side) { // side 0 = -X, 1 = +X ex = side == 0 ? out_x0 - 10 : out_x0 + out_w; cy = (out_y0 + out_y0 + out_d) / 2; difference() { translate([ex, cy - 7, -floor_t]) cube([10 + 0.01, 14, 3]); translate([side == 0 ? ex + 4.5 : ex + 5.5, cy, -floor_t - 1]) cylinder(d = ear_hole, h = 5); } } // ---------------- Lid (modelled in place; flip top-down to print) ----- module lid() { lz = base_h - floor_t; // underside of lid = top of walls difference() { union() { translate([0,0,lz]) rbox(out_x0, out_y0, out_w, out_d, lid_t, outer_r); // locating lip, notched around the posts translate([0,0,lz - 2]) difference() { rbox(in_x0 + fit, in_y0 + fit, in_w - 2*fit, in_d - 2*fit, 2.01, max(0.5, outer_r - wall)); translate([0,0,-1]) rbox(in_x0 + fit + 1.6, in_y0 + fit + 1.6, in_w - 2*fit - 3.2, in_d - 2*fit - 3.2, 4, 0.5); for (p = posts) translate([p[0], p[1], -1]) cylinder(r = post_r + 0.6, h = 4); // leave the ribbon notch clear (ribbon folds over the IDC under the lid) if (ribbon) translate([header_cx - ribbon_w/2, in_y0 - 1, -1]) cube([ribbon_w, 4, 4]); } } for (p = posts) translate([p[0], p[1], lz - 1]) { cylinder(d = m3_clear, h = lid_t + 2); if (countersink) translate([0,0,1 + lid_t - 1.7]) cylinder(d1 = m3_clear, d2 = 6.4, h = 1.71); } if (lid_screw_slots) for (t = term_x) translate([t[0] + 0.5, term_screw_y - screw_slot_w/2, lz - 3]) linear_extrude(lid_t + 4) offset(r = 1) offset(delta = -1) square([t[1] - t[0] - 1, screw_slot_w]); if (led_windows) for (l = leds) translate([l[0] - l[2]/2, l[1] - l[3]/2, lz - 1]) cube([l[2], l[3], lid_t + 2]); if (lid_labels) { for (i = [0 : len(leds) - 1]) translate([leds[i][0], leds[i][1] + leds[i][3]/2 + 1.5, lz + lid_t - 0.6]) linear_extrude(1) text(led_names[i], size = 3, halign = "center", font = "Liberation Sans:style=Bold"); translate([(in_x0 + in_x1)/2, in_y1 - 5.5, lz + lid_t - 0.6]) linear_extrude(1) text("RELAY TERMINALS", size = 3.5, halign = "center", font = "Liberation Sans:style=Bold"); } } } // ---------------- Small parts ----------------------------------------- module spacer() { // goes between Pi and HAT on the two header-side holes difference() { cylinder(d = 5, h = stack_gap); translate([0,0,-1]) cylinder(d = 2.8, h = stack_gap + 2); } } module cable_plug() { // closes the USB window around the cable; print flange-down w = usb_window[0] - 0.3; h = usb_window[1] - 0.3; difference() { union() { translate([-w/2 - 2, -h/2 - 2, 0]) cube([w + 4, h + 4, 1.2]); // outer flange translate([-w/2, -h/2, 0]) cube([w, h, 1.2 + wall]); // plug body } translate([0, 0, -1]) cylinder(d = 4.2, h = 10); // cable hole translate([-1.6, -h/2 - 3, -1]) cube([3.2, h/2 + 3, 10]); // slip-on slot } } // ---------------- Board ghosts (assembly view only) ------------------- module boards() { color("green", 0.9) translate([0,0,pi_z0]) difference() { linear_extrude(pi_t) rrect(pi_x0, pi_y0, pi_w, pi_d, board_r); for (h = pi_holes) translate([h[0], h[1], -1]) cylinder(d = 2.75, h = 5); } color("silver") for (x = [usb_pwr_x, usb_otg_x]) translate([x - 4, pi_port_y - 5, pi_top]) cube([8, 5.5, 2.6]); color("black") translate([4.87 - 1.27, -2.54, pi_top]) cube([50.8, 5.08, stack_gap]); // header color("navy", 0.9) translate([0,0,hat_z0]) difference() { linear_extrude(hat_t) rrect(hat_x0, hat_y0, hat_w, hat_d, board_r); for (h = hat_holes) translate([h[0], h[1], -1]) cylinder(d = 2.75, h = 5); } for (t = term_x) { color("deepskyblue") translate([t[0], hat_y0 + hat_d - 11.5, hat_top]) cube([t[1] - t[0], 10, term_h]); color("dimgray") translate([t[0], hat_y0 + hat_d - 33, hat_top]) cube([t[1] - t[0], 19, relay_h]); } color("orange") for (h = [pi_holes[0], pi_holes[1]]) translate([h[0], h[1], pi_top]) spacer(); if (ribbon) ribbon_ghost(); } module ribbon_ghost() { // IDC socket + ribbon folded toward the -Y wall color("dimgray") translate([header_cx - 27.95, -3.2, hat_top]) cube([55.9, 6.4, idc_h]); rz = inner_h - ribbon_slot_h + 0.8; // ribbon runs up out of the IDC, then out the notch color("lightgray") { translate([header_cx - 25.5, -0.65, hat_top + idc_h]) cube([51, 1.3, rz - hat_top - idc_h + 1.3]); translate([header_cx - 25.5, in_y0 - wall - 15, rz]) cube([51, -in_y0 + wall + 15 + 0.65, 1.3]); } } // ---------------- KiCad overlay check --------------------------------- // Red = outlines exported from KiCad (kicad/stack_outline.dxf), // green = this file's board data. They should coincide exactly. module kicad_check() { color("red") linear_extrude(0.4) import("kicad/stack_outline.dxf"); color("lime", 0.6) translate([0,0,0.4]) linear_extrude(0.4) { difference() { rrect(pi_x0, pi_y0, pi_w, pi_d, board_r); offset(-0.3) rrect(pi_x0, pi_y0, pi_w, pi_d, board_r); } difference() { rrect(hat_x0, hat_y0, hat_w, hat_d, board_r); offset(-0.3) rrect(hat_x0, hat_y0, hat_w, hat_d, board_r); } for (h = concat(pi_holes, hat_holes)) translate(h) difference() { circle(d = 2.75); circle(d = 2.15); } } } // ---------------- Output ---------------------------------------------- if (part == "assembly") { base(); boards(); color("white", 0.35) translate([0, 0, 25]) lid(); } else if (part == "base") translate([0, 0, floor_t]) base(); else if (part == "lid") lid_flipped(); else if (part == "spacer") for (i = [0:3]) translate([i * 8, 0, 0]) spacer(); else if (part == "cable_plug") cable_plug(); else if (part == "print_plate") { translate([0, 0, floor_t]) base(); translate([0, out_d + 15, 0]) lid_flipped(); for (i = [0:3]) translate([out_x0 + i * 8 + 3, -out_d / 2 - 2, 0]) spacer(); translate([out_x0 + 50, -out_d / 2 - 2, 0]) cable_plug(); } else if (part == "kicad_check") kicad_check(); // In-place pieces for the HTML viewer (assembled coordinates, Z = floor top) else if (part == "view_base") base(); else if (part == "view_lid") lid(); else if (part == "view_pi") { translate([0,0,pi_z0]) difference() { linear_extrude(pi_t) rrect(pi_x0, pi_y0, pi_w, pi_d, board_r); for (h = pi_holes) translate([h[0], h[1], -1]) cylinder(d = 2.75, h = 5); } for (x = [usb_pwr_x, usb_otg_x]) translate([x - 4, pi_port_y - 5, pi_top]) cube([8, 5.7, 2.6]); translate([hdmi_x - 5.6, pi_port_y - 7.5, pi_top]) cube([11.2, 7.6, 3.3]); translate([pi_x0 + pi_w - 12, sd_y - 6, pi_top]) cube([12.6, 12, 1.4]); } else if (part == "view_header") translate([4.87 - 1.27, -2.54, pi_top]) cube([50.8, 5.08, stack_gap]); else if (part == "view_hat") translate([0,0,hat_z0]) difference() { linear_extrude(hat_t) rrect(hat_x0, hat_y0, hat_w, hat_d, board_r); for (h = hat_holes) translate([h[0], h[1], -1]) cylinder(d = 2.75, h = 5); } else if (part == "view_relays") for (t = term_x) translate([t[0], hat_y0 + hat_d - 33, hat_top]) cube([t[1] - t[0], 19, relay_h]); else if (part == "view_terminals") for (t = term_x) translate([t[0], hat_y0 + hat_d - 11.5, hat_top]) difference() { cube([t[1] - t[0], 10, term_h]); for (i = [0:2]) translate([(t[1] - t[0])/2 + (i - 1) * 5.08, 6.5, term_h - 1]) cylinder(d = 3.2, h = 2); } else if (part == "view_ribbon") ribbon_ghost(); else if (part == "view_spacers") for (h = [pi_holes[0], pi_holes[1]]) translate([h[0], h[1], pi_top]) spacer(); else if (part == "view_plug") translate([usb_pwr_x, in_y1 + wall + 1.2, pi_top + 1.3]) rotate([90, 0, 0]) cable_plug(); // Lid rotated top-face-down and dropped onto the bed (Z=0) module lid_flipped() { lz = base_h - floor_t; translate([0, 0, lz + lid_t]) mirror([0, 0, 1]) lid(); }