onlyn00bs-badge

OnlyN00bs: a DEF CON 34 friend-finder badge. ESP32 firmware, Web Bluetooth setup app, printable case
git clone https://git.virtualshack.io/onlyn00bs-badge.git
Log | Files | Refs | README | LICENSE

gen_tophat.py (12741B)


      1 #!/usr/bin/env python3
      2 r"""
      3 gen_tophat.py — drop-on TOPHAT accessory for the badge case (novelty, sibling of gen_cat_ears).
      4 
      5 A hollow stovepipe tophat that sits astride the centred lanyard tab. The tab pokes up
      6 into the crown; the lanyard clip lives INSIDE the hollow and the cord threads out a slot
      7 in the crown top — so the badge looks like it's wearing a hat that's being lifted off by
      8 the lanyard. Loads from ABOVE (straight down over the tab), friction-held.
      9 
     10     FRONT (looking at the badge face-on)          SECTION (Z = case depth runs L→R)
     11 
     12             ╔═══════════════╗  crown, hollow          ╔═══════════════╗
     13             ║   ┌───┐ slot  ║                          ║               ║  <- flat top
     14             ║   │   │        ║   crown top with        ║   ┌───────┐   ║     w/ cord slot
     15             ║   │tab│  clip  ║   the cord slot         ║   │ hollow │   ║
     16        ═════╬═══╧═══╧════════╬═   hatband (2-col)      ║   │  ┌──┐  │   ║  <- fins grip the
     17         ▓▓▓ ║               ║                     ═════╬═══╪══▓▓══╪═══╬═     tab's 9mm faces
     18       ══╪═══╩═══════════════╩══╪══  brim on the        ▓▓▓║  │tab│  │  ║▓▓▓
     19         │        badge          │   top edge          ══╧══╧══╧══╧══╧══╧══  brim ↔ top edge
     20         │                       │                        0        22 (Z, tray-wall-top depth)
     21 
     22 RETENTION: two fins inside the crown hug the tab's two 9mm-apart faces at a slide-fit
     23 (TOL/side, a PETG push fit like the cat-ears frame). They're tied into the crown wall by
     24 low webs so they aren't loose islands, and they grip over the tab's ~6mm straight run
     25 (below the tab's rounded top). The brim bottoms out on the badge top edge — that flat
     26 seat plus the tab's 22mm depth is what stops the hat pitching fore/aft. Anti-rotation is
     27 free: the tab is a rectangular key.
     28 
     29 LANYARD ROUTING (v0.1): the cord loops OVER the tab's rounded top inside the hollow and
     30 exits the crown-top slot; the clip stays captured below the slot. This bypasses the tab's
     31 side-to-side bore (which the fins cover), so the hang point rises a few mm above the
     32 bore's CoG-tuned line — fine for a novelty, may add a touch of pitch. A future variant can
     33 split the fins with a bore relief to keep the tuned interface.
     34 
     35 Badge dimensions are IMPORTED from gen_stl (plate_outline / cavity_depth / P / ear_*), so
     36 the hat can never drift out of sync with the tab it wraps.
     37 
     38 BUILD: the round bodies of revolution (brim, crown tube, roof, hatband) are built in a
     39 local Z-up frame with annulus_slab and rotated onto the badge (local +Z → world +Y, a
     40 det-+1 rotation so the watertight normals survive). The rectangular fins/webs are plain
     41 world-space boxes. Everything composes by OVERLAP (same trick as gen_stl's bosses / the
     42 lanyard tab): each piece is its own closed solid, the slicer unions them, so the reported
     43 mesh has expected non-manifold seams at the overlaps — that's not a defect here.
     44 
     45 Print BRIM-DOWN, crown up: the opening is on the bed (no bridge there), fins and brim grow
     46 straight up. The only overhang is the flat crown top — a ~29mm bridge broken by the slot;
     47 add a few support lines or accept minor interior sag (the top SURFACE prints clean).
     48 
     49 Out: ./stl/accessories/tophat_<ver>.stl
     50 """
     51 
     52 import math
     53 import os
     54 import sys
     55 
     56 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
     57 
     58 from gen_stl import (                                    # noqa: E402
     59     P, plate_outline, cavity_depth,
     60     ensure_ccw, rounded_rect, circle_loop, perimeter_sample, annulus_slab, box,
     61     write_stl, bbox, edge_manifold_issues, mesh_volume_cc, signed_area,
     62     _DENSITY_G_CC, git_rev,
     63 )
     64 
     65 # ----------------------------------------------------------------------------
     66 # PARAMS — the hat's own numbers. Everything about the BADGE/tab is imported.
     67 # ----------------------------------------------------------------------------
     68 VERSION = "v0.1"
     69 LAYOUT = "B"                # the production layout
     70 
     71 # --- the hat silhouette --------------------------------------------------------
     72 CROWN_OD = 34.0             # crown outer diameter
     73 WALL = 2.6                  # crown/brim/roof wall thickness
     74 CROWN_H = 36.0             # total hat height above the badge top edge
     75 CROWN_TOP_T = 2.6          # thickness of the flat crown top (the roof)
     76 BRIM_OD = 48.0             # brim outer diameter
     77 BRIM_T = 3.0               # brim thickness
     78 
     79 # --- the cord/clip exit slot in the crown top ---------------------------------
     80 CLIP_W = 22.35             # measured lanyard clip-mechanism width (the long side must clear it)
     81 SLOT_CLEAR = 1.0           # clearance added around the clip on the long side (X)
     82 SLOT_HX = (CLIP_W + SLOT_CLEAR) / 2.0   # long half-width across the badge (X) -> 23.35mm slot
     83 SLOT_HY = 3.5             # short half-depth (badge Z) -> 7mm slot (+1mm wiggle over the old 6mm)
     84 SLOT_R = 2.5              # slot corner rounding
     85 
     86 # --- the hatband (a proud ring, a 2nd-colour swap target like the ear recess) --
     87 BAND = True
     88 BAND_PROUD = 1.2          # how far the band stands off the crown
     89 BAND_Z0, BAND_Z1 = BRIM_T - 0.4, BRIM_T + 3.6   # sit the band ON the brim (0.4 overlap) so its
     90                           #   proud underside is backed by the brim top — no floating ledge, so
     91                           #   no support material in a brim↔band gap (printed brim-down)
     92 
     93 # --- the tab socket (retention) ------------------------------------------------
     94 TOL = 0.30                # per-side slide clearance, fin ↔ tab face. Push fit in PETG.
     95 FIN_T = 2.2              # fin thickness (outboard of the grip face)
     96 FIN_H = 7.0             # fin height above the top edge (grips the tab's ~6mm straight run)
     97 FIN_Z_INSET = 2.0       # fin ends held this far off the tab's front/back faces
     98 WEB_H = 5.0             # tie-web height (fin outer face -> crown inner wall)
     99 WEB_Z0, WEB_Z1 = 6.0, 16.0   # tie-web depth span (kept where a straight box stays inside
    100                               #   the round wall — see the derivation below)
    101 WEB_EMBED = 1.6          # how far the web reaches past the crown inner wall (into material)
    102 
    103 SEG = 96                 # circle resolution for the revolved bodies
    104 
    105 MATERIAL = P["material"]
    106 INFILL = P["infill"]
    107 
    108 # ----------------------------------------------------------------------------
    109 # DERIVED — badge/tab geometry, straight from gen_stl. No magic numbers.
    110 # ----------------------------------------------------------------------------
    111 W, H = plate_outline(LAYOUT)                       # badge face, mm (top edge at y=H)
    112 TAB_DEPTH = P["plate_t"] + cavity_depth(LAYOUT)    # tab spans z 0..TAB_DEPTH (tray wall top)
    113 CX = W / 2.0                                        # hat axis, x (tab is centred)
    114 CZ = TAB_DEPTH / 2.0                                # hat axis, z (tab depth centre)
    115 
    116 EAR_W = P["ear_w"]                                 # tab width (x)
    117 EAR_H = P["ear_h"]                                 # tab protrusion above the top edge (y)
    118 EAR_R = P["ear_corner_r"]                          # tab top-corner rounding
    119 TAB_X0 = (W - EAR_W) / 2.0                          # tab faces in x
    120 TAB_X1 = TAB_X0 + EAR_W
    121 
    122 CROWN_ID = CROWN_OD - 2 * WALL                     # crown inner diameter
    123 ROOF_OR = CROWN_ID / 2.0 + 0.5                     # roof plug outer radius (0.5 into the wall)
    124 
    125 # fin x-bands: the grip faces sit TOL outboard of the tab faces
    126 FIN_L_X1 = TAB_X0 - TOL                             # left fin inner (grip) face
    127 FIN_L_X0 = FIN_L_X1 - FIN_T
    128 FIN_R_X0 = TAB_X1 + TOL                             # right fin inner (grip) face
    129 FIN_R_X1 = FIN_R_X0 + FIN_T
    130 FIN_Z0 = CZ - TAB_DEPTH / 2.0 + FIN_Z_INSET        # = FIN_Z_INSET
    131 FIN_Z1 = CZ + TAB_DEPTH / 2.0 - FIN_Z_INSET
    132 
    133 OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stl", "accessories")
    134 
    135 
    136 # ----------------------------------------------------------------------------
    137 # local Z-up build -> world (local +Z is the hat axis -> world +Y, up out of the
    138 # top edge). Rotation about world X by -90deg (det +1) so annulus_slab's
    139 # by-construction normals survive; then translate the axis to (CX, H, CZ).
    140 # ----------------------------------------------------------------------------
    141 
    142 def _to_world(tris):
    143     def m(p):
    144         lx, ly, lz = p
    145         return (lx + CX, lz + H, -ly + CZ)
    146     return [(m(a), m(b), m(c)) for a, b, c in tris]
    147 
    148 
    149 def _slot_loop(k):
    150     """The cord-exit slot as a CCW loop of k points (a rounded rect centred on the axis)."""
    151     rr = ensure_ccw(rounded_rect(-SLOT_HX, -SLOT_HY, SLOT_HX, SLOT_HY, SLOT_R, 16))
    152     return perimeter_sample(rr, k)
    153 
    154 
    155 # ----------------------------------------------------------------------------
    156 # build
    157 # ----------------------------------------------------------------------------
    158 
    159 def make_hat():
    160     local = []          # tris built in the local Z-up frame (revolved bodies)
    161 
    162     # --- brim: a flat ring; inner = crown bore so the tab/fins pass through -------
    163     brim_out = circle_loop(0, 0, BRIM_OD / 2.0, SEG)
    164     brim_in = circle_loop(0, 0, CROWN_ID / 2.0, SEG)
    165     local += annulus_slab(ensure_ccw(brim_out), ensure_ccw(brim_in), 0.0, BRIM_T)
    166 
    167     # --- crown tube: the stovepipe wall (overlaps the brim by 0.4) ----------------
    168     crown_out = circle_loop(0, 0, CROWN_OD / 2.0, SEG)
    169     crown_in = circle_loop(0, 0, CROWN_ID / 2.0, SEG)
    170     local += annulus_slab(ensure_ccw(crown_out), ensure_ccw(crown_in), BRIM_T - 0.4, CROWN_H)
    171 
    172     # --- roof: the flat top, plugging the bore from inside, with the cord slot -----
    173     roof_out = circle_loop(0, 0, ROOF_OR, SEG)     # 0.5 into the wall -> no coincident face
    174     roof_in = _slot_loop(SEG)
    175     local += annulus_slab(ensure_ccw(roof_out), ensure_ccw(roof_in),
    176                           CROWN_H - CROWN_TOP_T, CROWN_H)
    177 
    178     # --- hatband: a proud ring (2nd-colour swap) ----------------------------------
    179     if BAND:
    180         band_out = circle_loop(0, 0, CROWN_OD / 2.0 + BAND_PROUD, SEG)
    181         band_in = circle_loop(0, 0, CROWN_OD / 2.0 - 0.4, SEG)   # 0.4 into the wall
    182         local += annulus_slab(ensure_ccw(band_out), ensure_ccw(band_in), BAND_Z0, BAND_Z1)
    183 
    184     tris = _to_world(local)
    185 
    186     # --- retention: two grip fins + two tie-webs, in WORLD space ------------------
    187     y0 = H
    188     y_fin1 = H + FIN_H
    189     y_web1 = H + WEB_H
    190     # fins hug the tab's x-faces over its depth
    191     tris += box(FIN_L_X0, y0, FIN_Z0, FIN_L_X1, y_fin1, FIN_Z1)
    192     tris += box(FIN_R_X0, y0, FIN_Z0, FIN_R_X1, y_fin1, FIN_Z1)
    193     # webs tie each fin's outer face into the crown inner wall (embed WEB_EMBED past it)
    194     wall_l = CX - CROWN_ID / 2.0                     # crown inner wall x at the centre plane
    195     wall_r = CX + CROWN_ID / 2.0
    196     tris += box(wall_l - WEB_EMBED, y0, WEB_Z0, FIN_L_X1, y_web1, WEB_Z1)
    197     tris += box(FIN_R_X0, y0, WEB_Z0, wall_r + WEB_EMBED, y_web1, WEB_Z1)
    198 
    199     return tris
    200 
    201 
    202 def main():
    203     os.makedirs(OUT_DIR, exist_ok=True)
    204     tris = make_hat()
    205 
    206     path = os.path.join(OUT_DIR, "tophat_%s.stl" % VERSION)
    207     write_stl(path, tris, "badge tophat %s / case %s" % (VERSION, P["case_version"]))
    208 
    209     bw, bh, bd = bbox(tris)
    210     vol = mesh_volume_cc(tris)                        # approx: overlaps double-count a little
    211     grams = vol * _DENSITY_G_CC[MATERIAL] * INFILL
    212     bad = edge_manifold_issues(tris)
    213 
    214     print("tophat %s  (badge case %s, layout %s, git %s)" % (
    215         VERSION, P["case_version"], LAYOUT, git_rev() or "?"))
    216     print("  sits on tab      %.1f(x) x %.1f(up) x %.1f(z-depth) mm  at badge top-centre" % (
    217         EAR_W, EAR_H, TAB_DEPTH))
    218     print("  hat envelope     %.1f x %.1f x %.1f mm  (%d tris)" % (bw, bh, bd, len(tris)))
    219     print("  crown            OD %.1f, ID %.1f, height %.1f, wall %.1f" % (
    220         CROWN_OD, CROWN_ID, CROWN_H, WALL))
    221     print("  brim             OD %.1f, thick %.1f" % (BRIM_OD, BRIM_T))
    222     print("  cord slot        %.1f x %.1f mm (r%.1f) in the crown top" % (
    223         2 * SLOT_HX, 2 * SLOT_HY, SLOT_R))
    224     print("  tab socket       fins %.1f thick, %.2f/side slide, grip %.1f mm tall%s" % (
    225         FIN_T, TOL, FIN_H, ", hatband band" if BAND else ""))
    226     print("  material         %s @ %.0f%% -> ~%.1f cc, ~%.0f g  (approx, overlap-composed)" % (
    227         MATERIAL, INFILL * 100, vol, grams))
    228     print("  manifold         %s" % ("OK (closed)" if bad == 0 else
    229                                      "%d open edges (overlap-composed: brim/crown/roof/fins union)" % bad))
    230     print("  -> %s" % path)
    231 
    232 
    233 if __name__ == "__main__":
    234     main()