From f6ecc63254eeee327f1f158acc2d7e460478ac6e Mon Sep 17 00:00:00 2001 From: Sam Hadow Date: Fri, 9 Oct 2026 19:51:17 +0200 Subject: [PATCH] esp32 S3 DevKit C1 case --- README.md | 19 +++- esp32/esp32s3_devkitc1_case.scad | 164 +++++++++++++++++++++++++++++++ esp32s3_devkitc1_case.py | 73 ++++++++++++++ utils.py | 16 +++ 4 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 esp32/esp32s3_devkitc1_case.scad create mode 100755 esp32s3_devkitc1_case.py diff --git a/README.md b/README.md index 98757c4..87b93dc 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ python flower_pot.py ### weave_planter Parameterized round weave planter with a pot and a tray. -To generate the pot and tray STL files: +To generate the pot and tray STL file: ```bash python weave_planter.py ``` @@ -23,7 +23,7 @@ python weave_planter.py ### spiral_pot Parameterized round self watering spiral pot. Includes an inner pot and an outer pot with a reservoir and a spiral pattern. (It takes a long time to render). -To generate the STL files: +To generate the STL file: ```bash python spiral_pot.py ``` @@ -36,18 +36,29 @@ To generate the tray and plates STL files: python seedling_tray.py ``` +## ESP32 + +### esp32s3_devkitc1_case + +Snap in case in 2 parts for an ESP32 S3 DevKit C1. Check your board dimensions against the ones in the script. +To generate the STL file: +```bash +python esp32s3_devkitc1_case.py +``` + + ## other ### parametric_box Parameterized rectangular box with a clip in lid. -To generate the box and lid STL files: +To generate the box and lid STL file: ```bash python parametric_box.py ``` ### pill_container Parameterized pill containers with a sliding lid. -To generate the box and lid STL files: +To generate the box and lid STL file: ```bash python pill_container.py ``` diff --git a/esp32/esp32s3_devkitc1_case.scad b/esp32/esp32s3_devkitc1_case.scad new file mode 100644 index 0000000..19f8512 --- /dev/null +++ b/esp32/esp32s3_devkitc1_case.scad @@ -0,0 +1,164 @@ +// ===================================================================== +// ESP32-S3-DevKitC-1 - two-part snap-fit case +// +// BASE : floor + walls. Board rests on 4 round posts, on the inner side of the pin slots, lined up in X with the first and last pin. Pin rows poke down into two floor slots, so Dupont connectors plug in from underneath. +// LID : cap with a skirt that clips over the base's inner "tongue". Has the two button holes. USB-C opening is split between both parts, so neither part needs a bridged (unsupported) cut-out. +// +// +// !! Measure your board and check the values marked [CHECK] !! +// Set show_board = true and part = "assembled" to preview the fit. +// ===================================================================== + +part = "print"; // "print" | "base" | "lid" | "assembled" +show_board = true; // ghost board in the "assembled" view +explode = 0; // mm - lifts the lid in the "assembled" view + +$fn = 64; + +/* ---------------- Board ---------------- */ +pcb_l = 63.74; // [CHECK] PCB length (USB-C end -> antenna end) +pcb_w = 28.35; // [CHECK] PCB width +pcb_t = 1.6; // PCB thickness +pin_below = 11; // [CHECK] PCB underside -> pin tips (header + pin) +header_pins = 22; // pins per row +pin_pitch = 2.54; +row_dist = 24.86; // distance between the two pin rows +header_x0 = 2; // [CHECK] first pin CENTRE, measured from the USB end +comp_h = 3.5; // tallest part above PCB top + +/* ---------------- USB-C opening ---------------- */ +usb_slot_w = 24; // one wide slot for both connectors + cable plug overmolds +usb_plug_h = 6.5; // slot height (plug overmold) +usb_axis_h = 1.7; // connector axis height above PCB top + +/* ---------------- Buttons (lid holes) ---------------- */ +// [CHECK] position of each button centre: [distance from USB end edge, offset from board centre line] +boot_pos = [28, -5.68]; +rst_pos = [33, -5.68]; +btn_d = 3.4; // hole diameter (use a pin / paper-clip / toothpick) + +/* ---------------- Case / print tuning ---------------- */ +wall = 2.8; // outer wall +tongue_t = 1.6; // inner lip of the base (the part that carries the snap groove) +tol = 0.2; // sliding clearance for PETG +clr = 0.4; // board-to-wall clearance +floor_t = 2.0; +plate_t = 2.0; // lid top thickness +top_gap = 0.6; // clearance above the tallest component +overlap = 6; // how far the lid skirt slips over the base +snap_d = 0.5; // snap depth - raise to 0.6 for a firmer click +snap_len = 12; // length of each snap bump +r_in = 1.0; // inner corner radius + +pin_win_w = 3.0; // floor slot width under each pin row (Dupont housing ~2.6) +pin_win_margin = 2.0; // slot overhang beyond first/last pin +post_d = 3; +post_y = 8; // post offset from centre line (inner side of the pin rows, between the header bodies) +// post X positions: in line with the first and last pin of the rows +post_x = [header_x0, header_x0 + (header_pins-1)*pin_pitch]; + +/* ---------------- Derived ---------------- */ +L_i = pcb_l + 2*clr; +W_i = pcb_w + 2*clr; +z_pcb_bot = pin_below + 0.5; // pin tips end 0.5 mm above the print bed +z_pcb_top = z_pcb_bot + pcb_t; +z_split = z_pcb_top + 2; // joint between base and lid +z_ceil = z_pcb_top + comp_h + top_gap; +z_top = z_ceil + plate_t; +z_usb_bot = z_pcb_top + usb_axis_h - usb_plug_h/2; +z_usb_top = z_pcb_top + usb_axis_h + usb_plug_h/2; +z_snap = z_split - 3.3; +ridge_d = snap_d + tol - 0.1; +snap_x = [0.27*L_i, 0.73*L_i]; + +/* ---------------- Helpers ---------------- */ +module rr(x0, x1, y0, y1, r) { + translate([x0+r, y0+r]) offset(r=r) square([x1-x0-2*r, y1-y0-2*r]); +} +module outline(d=0) { rr(-d, L_i+d, -W_i/2-d, W_i/2+d, r_in+d); } +module slab(d, z0, z1) { translate([0,0,z0]) linear_extrude(z1-z0) outline(d); } + +// 45-degree triangular prism along X; apex points toward -Y (into the case) +module wedge(len, d) { + rotate([90,0,90]) linear_extrude(len, center=true) + polygon([[-d,0],[0.3,d+0.3],[0.3,-d-0.3]]); +} +// all snap positions: two per long side + one on the far end wall +module snap_set(off, d) { + for (xc = snap_x, s = [-1,1]) + translate([xc, s*(W_i/2 + tongue_t + off), z_snap]) + mirror([0, s<0 ? 1 : 0, 0]) wedge(snap_len, d); + translate([L_i + tongue_t + off, 0, z_snap]) + rotate([0,0,-90]) wedge(snap_len, d); +} + +/* ---------------- BASE ---------------- */ +module base() { + difference() { + union() { + difference() { + slab(wall, 0, z_split); + slab(0, floor_t, z_split+1); // pocket + translate([0,0,z_split-overlap]) // rebate -> leaves the tongue + linear_extrude(overlap+1) + difference() { outline(wall+1); outline(tongue_t); } + snap_set(0, snap_d); // snap grooves + } + for (px = post_x, s = [-1,1]) // PCB support posts (in line with first / last pin) + translate([clr+px, s*post_y, floor_t-0.01]) + cylinder(d=post_d, h=z_pcb_bot-floor_t+0.01); + } + // pin-row slots through the floor + for (s = [-1,1]) + translate([clr + header_x0 - pin_win_margin, s*row_dist/2 - pin_win_w/2, -1]) + cube([(header_pins-1)*pin_pitch + 2*pin_win_margin, pin_win_w, floor_t+2]); + // lower half of the USB-C opening (open at the top -> no bridging) + translate([-wall-1, -usb_slot_w/2, z_usb_bot]) + cube([wall+1, usb_slot_w, z_split+1-z_usb_bot]); + } +} + +/* ---------------- LID (modelled in assembled position) ---------------- */ +module lid() { + difference() { + union() { + slab(wall, z_split, z_top); // cap + translate([0,0,z_split-overlap+0.3]) // skirt over the tongue + linear_extrude(overlap-0.3+0.01) + difference() { outline(wall); outline(tongue_t+tol); } + snap_set(tol, ridge_d); // snap ridges + } + slab(0, z_split-1, z_ceil); // cavity + // upper half of the USB-C opening (open at the skirt edge -> no bridging) + translate([-wall-1, -usb_slot_w/2, z_split-overlap-1]) + cube([wall+1, usb_slot_w, z_usb_top-(z_split-overlap-1)]); + // BOOT / RST holes + for (p = [boot_pos, rst_pos]) + translate([clr+p[0], p[1], z_ceil-1]) cylinder(d=btn_d, h=plate_t+2); + } +} +module lid_for_print() { translate([0,0,z_top]) rotate([180,0,0]) lid(); } + +/* ---------------- Preview ghost (approximate) ---------------- */ +module ghost() { + color("seagreen", 0.6) { + translate([clr, -pcb_w/2, z_pcb_bot]) cube([pcb_l, pcb_w, pcb_t]); + for (s=[-1,1]) translate([clr+header_x0-1.27, s*row_dist/2-1.27, z_pcb_bot-2.5]) + cube([(header_pins-1)*pin_pitch+2.54, 2.54, 2.5]); + for (s=[-1,1]) for (i=[0:header_pins-1]) + translate([clr+header_x0+i*pin_pitch-0.32, s*row_dist/2-0.32, z_pcb_bot-pin_below]) + cube([0.64,0.64,pin_below]); + } + color("silver", 0.8) { + for (s=[-1,1]) translate([clr-0.5, s*6.5-4.47, z_pcb_top]) cube([7.3, 8.94, 3.2]); // USB-C (approx. y) + translate([clr+pcb_l-25.5, -9, z_pcb_top]) cube([25.5, 18, 3.2]); // module + } + color("black", 0.8) for (p=[boot_pos, rst_pos]) + translate([clr+p[0], p[1], z_pcb_top]) cylinder(d=3, h=3.5); +} + +/* ---------------- Output ---------------- */ +if (part == "base") base(); +else if (part == "lid") lid_for_print(); +else if (part == "assembled") { base(); translate([0,0,explode]) lid(); if (show_board) ghost(); } +else { base(); translate([0, -(W_i + 2*wall + 10), 0]) lid_for_print(); } diff --git a/esp32s3_devkitc1_case.py b/esp32s3_devkitc1_case.py new file mode 100755 index 0000000..55c36fe --- /dev/null +++ b/esp32s3_devkitc1_case.py @@ -0,0 +1,73 @@ +#!/bin/python3 +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +from utils import check_openscad, ask_optional_float + + +BASE_DIR = Path(__file__).resolve().parent +SCAD_DIR = BASE_DIR / "esp32" +SCAD_FILE = SCAD_DIR / "esp32s3_devkitc1_case.scad" +OUT_DIR = BASE_DIR / "stl" + +BOARD_PARAMS = [ + ("pcb_l", "PCB length, USB-C end to antenna end (pcb_l) in mm"), + ("pcb_w", "PCB width (pcb_w) in mm"), + ("pin_below", "PCB underside to pin tips (pin_below) in mm"), + ("header_x0", "First pin centre, measured from the USB end (header_x0) in mm"), +] + + +def render_scad(scad_file: Path, out_file: Path, part: str, overrides: dict[str, float]) -> None: + cmd = ["openscad", "-D", f'part="{part}"'] + for name, value in overrides.items(): + cmd += ["-D", f"{name}={value}"] + cmd += ["-o", str(out_file), str(scad_file)] + + result = subprocess.run( + cmd, + cwd=BASE_DIR, + text=True, + capture_output=True, + ) + + if result.returncode != 0: + print(f"\nFailed to render {part} from {scad_file.name}") + if result.stdout: + print(result.stdout) + if result.stderr: + print(result.stderr, file=sys.stderr) + raise SystemExit(result.returncode) + + print(f"Generated {out_file}") + + +def main() -> None: + check_openscad() + + if not SCAD_DIR.exists(): + raise SystemExit(f"Missing SCAD directory: {SCAD_DIR}") + if not SCAD_FILE.exists(): + raise SystemExit(f"Missing file: {SCAD_FILE}") + + print("ESP32-S3-DevKitC-1 case generator (base + lid).") + print("Measure your board and enter the values, or press Enter to keep the defaults of the .scad file.\n") + + overrides: dict[str, float] = {} + for name, prompt in BOARD_PARAMS: + value = ask_optional_float(f"{prompt}") + if value is not None: + overrides[name] = value + + OUT_DIR.mkdir(parents=True, exist_ok=True) + + render_scad(SCAD_FILE, OUT_DIR / "esp32s3_devkitc1_case.stl", "print", overrides) + + print("\nDone.") + + +if __name__ == "__main__": + main() diff --git a/utils.py b/utils.py index 0d9ccac..e73703d 100644 --- a/utils.py +++ b/utils.py @@ -28,6 +28,22 @@ def ask_float(prompt: str) -> float: except ValueError: print("Please enter a valid number.") +def ask_optional_float(prompt: str) -> float | None: + """Prompt user for an optional positive float.""" + while True: + raw = input(f"{prompt} [Enter = keep default]: ").strip().replace(",", ".") + if not raw: + return None + try: + value = float(raw) + except ValueError: + print("Please enter a number.") + continue + if value <= 0: + print("Value must be greater than 0.") + continue + return value + def ask_yes_no(prompt: str) -> bool: """Prompt the user for a yes/no answer.""" while True: