esp32 S3 DevKit C1 case

This commit is contained in:
sam.hadow committed 2026-10-09 19:51:17 +02:00
1 parent c8df6abdef
commit f6ecc63254
4 files changed
+268 -4

No files matched your search

+15 -4
View File
@@ -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
```
+164
View File
@@ -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(); }
+73
View File
@@ -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()
+16
View File
@@ -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: