"""Storage tray for spare ESP32 DevKit V1 boards, pins down and covered. Each bay is the seat out of the case: pins drop into blind trenches, the moulded header strips drop into the pocket that locates the board, and nothing sharp is left in the open. What the case does with a lid, the tray does with four spring tabs, one at each corner-ish end, that snap over the board's end edges and preload it so it cannot rattle. Why the ends and not the sides: the board's *length* is repeatable to about a third of a millimetre across this pattern, its *width* is not (51.4 to 51.74 long, but 28.3 to 29.0 wide), and a fixed lip cannot catch a 0.7 mm spread. See case.py, which cuts the cavity for the widest board of the pattern for the same reason. Run: .venv/bin/python tray.py -> out/tray_*.stl """ import os import numpy as np import case as C from manifold3d import Manifold # ---- the board comes from case.py, so the two parts can never drift apart CLR = 0.15 # board to wall FLOOR = 1.6 AIR = 1.8 # leaves 1.0 mm under the pin tips once the pocket is cut WALL = 1.6 # wall across the board's width GAP = 0.8 # relief behind a spring tab, so it has somewhere to bend TAB_T = 1.0 # tab blade: thin on purpose, it has to flex 0.6 mm TAB_W = 5.0 TAB_Y = 11.5 # tab centres, clear of the USB shell and of the module LIP = 0.35 # how far the lip reaches over the board's top edge LIP_H = 1.2 SET_IN = 0.30 # how far the blade stands inside the board's end, the preload RIB = 4.6 # rail along each long edge, carrying pocket and trench DIP = 7.0 # radius of the scallop that lets you pinch a board out R_OUT = 3.0 SEG = 64 # ---- levels, measured from the outside of the floor RAIL = FLOOR + C.PIN_BELOW + AIR # top of the rails, 8.6 SEAT = RAIL - C.POCKET # the header plastic rests here PCB_B = SEAT + C.HDR_PLASTIC PCB_T = PCB_B + C.BT RIM = PCB_T + C.COMP_H + 1.0 # walls clear the module, so trays stack IL, IW = C.BL + 2 * CLR, C.BW + 2 * CLR XB = C.BL / 2 - SET_IN # the blade's face at rest WX_OUT = TAB_T + GAP + WALL # wall at the ends of a bay WX_IN = 2 * (TAB_T + GAP) + WALL # a shared end wall carries two tabs rrect, slab, box, cyl = C.rrect, C.slab, C.box, C.cyl def size(nx, ny): return (nx * IL + 2 * WX_OUT + (nx - 1) * WX_IN, ny * IW + (ny + 1) * WALL) def centres(nx, ny): L, W = size(nx, ny) for i in range(nx): x = -L / 2 + WX_OUT + IL / 2 + i * (IL + WX_IN) for j in range(ny): y = -W / 2 + WALL + IW / 2 + j * (IW + WALL) yield x, y def bay_cut(x, y): """Everything taken out of the block for one bay.""" m = slab(IL, IW, 0.5, RAIL, RIM + 1, x, y) # the board's room m += slab(IL, IW - 2 * RIB, 0.5, FLOOR, RAIL + 1, x, y) # hollow between the rails # the same seat as the case: a trench for the pins, and a channel for each # strip that runs out to the wall and most of the length of the bay, so a # strip that is not centred on its board still drops in isl = C.ROW - C.PL_W / 2 - C.PL_CLR for s in (-1, 1): yy = y + s * C.ROW m += box(x - C.SLOT_X, x + C.SLOT_X, # pin trench yy - C.SLOT_W / 2, yy + C.SLOT_W / 2, FLOOR, RAIL + 1) y0, y1 = sorted((y + s * isl, y + s * IW / 2)) # the wall, not past it m += box(x - C.SLOT_X, x + C.SLOT_X, y0, y1, SEAT, RAIL + 1) m += slab(26, 15, 3.0, -1, FLOOR + 1, x, y) # push a board back out # the USB-C shell hangs 1.5 mm past the board, so open the wall in front of it m += box(x - IL / 2 - WX_IN, x - IL / 2 + 1, y - 6, y + 6, PCB_T - 0.5, RIM + 1) return m def tab_slots(x, y): """The moat that makes each tab a cantilever instead of part of the wall.""" m = Manifold() for sx in (-1, 1): for sy in (-1, 1): yc = y + sy * TAB_Y x0, x1 = x + sx * XB, x + sx * (XB + TAB_T + GAP) m += box(min(x0, x1), max(x0, x1), yc - TAB_W / 2 - GAP, yc + TAB_W / 2 + GAP, FLOOR, RIM + 1) return m def tabs(x, y): """Four spring tabs per bay: a thin blade with a lip over the board's edge. The blade is rooted in the floor and stands in its own moat, SET_IN inside where the board's end will be, so every board preloads it a little and none of them rattle; the lip above the board is what stops one falling out when the tray is tipped. Insertion asks for about 0.65 mm of bend over 12 mm of blade, which is 0.65% strain: a third of what would worry printed PLA, and the blade is thin (TAB_T) for exactly that reason. """ m = Manifold() for sx in (-1, 1): for sy in (-1, 1): yc = y + sy * TAB_Y x0, x1 = x + sx * XB, x + sx * (XB + TAB_T) lo, hi = min(x0, x1), max(x0, x1) m += box(lo, hi, yc - TAB_W / 2, yc + TAB_W / 2, FLOOR, PCB_T) # the lip, with a ramp on top the board can push out of its way m += box(lo - (LIP if sx > 0 else 0), hi + (LIP if sx < 0 else 0), yc - TAB_W / 2, yc + TAB_W / 2, PCB_T, PCB_T + LIP_H) ramp = Manifold.cube((2 * LIP, TAB_W + 1, 2 * LIP), center=True).rotate((0, 45, 0)) m -= ramp.translate((x + sx * (XB - LIP), yc, PCB_T + LIP_H)) return m def tray(nx=1, ny=3): L, W = size(nx, ny) body = slab(L, W, R_OUT, 0, RIM) for x, y in centres(nx, ny): body -= bay_cut(x, y) + tab_slots(x, y) body += tabs(x, y) # scallops in the long walls so a fingertip reaches the board's edge for x, y in centres(nx, ny): for s in (-1, 1): yw = y + s * (IW / 2 + WALL / 2) body -= Manifold.cylinder(WALL + 2, DIP, circular_segments=SEG, center=True).rotate((90, 0, 0)).translate((x, yw, RIM)) return body def main(): here = os.path.dirname(os.path.abspath(__file__)) out = os.path.join(here, "out") os.makedirs(out, exist_ok=True) for n, (nx, ny) in SHAPES.items(): t = tray(nx, ny) C.save(t, os.path.join(out, f"tray_{n}.stl")) L, W = size(nx, ny) print(f"tray_{n}: {nx}x{ny} bays, {L:.1f} x {W:.1f} x {RIM:.1f} mm, " f"{t.volume()/1000:.1f} cm3, genus {t.genus()}") SHAPES = {2: (1, 2), 3: (1, 3), 4: (2, 2), 6: (2, 3)} if __name__ == "__main__": main()