From 61c716ee58986ce3ca7986c107b5701746b39d90 Mon Sep 17 00:00:00 2001 From: sl-mechanical Date: Sun, 1 Mar 2026 00:41:45 -0500 Subject: [PATCH] feat: UWB tag enclosure + stem anchor mounts (#57, #61, #62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3× MaUWB ESP32-S3 follow-me UWB system: 1 wearable tag, 2 robot anchors. chassis/uwb_tag_enclosure.scad Belt-clip enclosure for MaUWB PCB (~50×25×10 mm) + TP4056 micro-USB charger + 18650 cell. Snap-fit PETG shell + TPU 95A bumper sleeve. IP44-ish 4 mm overlap + 2-turn labyrinth seam. Open antenna window in lid (no PLA within 10 mm of UWB antenna). Power switch cutout (Y− face), micro-USB port (X− face), LED window hole (Y+ face). Belt clip integrated (PETG spring arm, 42 mm belt slot). RENDER: body/lid/tpu_bumper/assembly. chassis/uwb_anchor_mount.scad Stem-mounted anchor bracket for 25 mm OD stem. Split D-collar with M4 thumbscrews (tool-free), M4 hex nut pockets, M4 set screw height lock. Anti-rotation flat tab on front half prevents axial rotation without stem modification. USB cable routing channel in rear half. Module bracket tilted 10° outward — antenna faces horizon, clears stem metal. Back-wall cutout behind antenna section (10 mm clearance). 250 mm anchor spacing (RENDER "pair" shows both on stem section). RENDER: collar_front/collar_rear/ bracket/assembly/pair. chassis/uwb_assembly.md Full assembly notes: antenna clearance rules, IP44 seam description, stem positioning diagram (anchors at 450 mm + 700 mm), USB cable routing, complete BOM (~300 g total, tag ~130 g). Co-Authored-By: Claude Sonnet 4.6 --- chassis/uwb_anchor_mount.scad | 275 ++++++++++++++++++++++++ chassis/uwb_assembly.md | 204 ++++++++++++++++++ chassis/uwb_tag_enclosure.scad | 369 +++++++++++++++++++++++++++++++++ 3 files changed, 848 insertions(+) create mode 100644 chassis/uwb_anchor_mount.scad create mode 100644 chassis/uwb_assembly.md create mode 100644 chassis/uwb_tag_enclosure.scad diff --git a/chassis/uwb_anchor_mount.scad b/chassis/uwb_anchor_mount.scad new file mode 100644 index 0000000..6c77443 --- /dev/null +++ b/chassis/uwb_anchor_mount.scad @@ -0,0 +1,275 @@ +// ============================================================ +// uwb_anchor_mount.scad — Stem-Mounted UWB Anchor Rev A +// Agent: sl-mechanical 2026-03-01 +// Closes issues #57, #62 +// ============================================================ +// Clamp-on bracket for 2× MaUWB ESP32-S3 anchor modules on +// SaltyBot 25 mm OD vertical stem. +// Anchors spaced ANCHOR_SPACING = 250 mm apart. +// +// Features: +// • Split D-collar with M4 clamping bolts + M4 set screw +// • Anti-rotation flat tab that keys against a small pin +// OR printed key tab that registers on the stem flat (if stem +// has a ground flat) — see ANTI_ROT_MODE parameter +// • Module bracket: faces outward, tilted 10° from vertical +// so antenna clears stem and faces horizon +// • USB cable channel (power from Orin via USB-A) on collar +// • Tool-free capture: M4 thumbscrews (slot-head, hand-tighten) +// • UWB antenna area: NO material within 10 mm of PCB top face +// +// Components per mount: +// 2× collar_half print in PLA/PETG, flat-face-down +// 1× module_bracket print in PLA/PETG, flat-face-down +// +// RENDER options: +// "assembly" single mount assembled (default) +// "collar_front" front collar half for slicing (×2 per mount × 2 mounts = 4) +// "collar_rear" rear collar half +// "bracket" module bracket (×2 mounts) +// "pair" both mounts on 350 mm stem section +// ============================================================ + +RENDER = "assembly"; + +// ── ⚠ Verify with calipers ─────────────────────────────────── +MAWB_L = 50.0; // PCB length +MAWB_W = 25.0; // PCB width +MAWB_H = 10.0; // PCB + components +MAWB_HOLE_X = 43.0; // M2 mounting hole X span +MAWB_HOLE_Y = 20.0; // M2 mounting hole Y span +M2_D = 2.2; // M2 clearance + +// ── Stem ───────────────────────────────────────────────────── +STEM_OD = 25.0; +STEM_BORE = 25.4; // +0.4 clearance +WALL = 2.0; // wall thickness (used in thumbscrew recess) + +// ── Collar ─────────────────────────────────────────────────── +COL_OD = 52.0; +COL_H = 30.0; // taller than sensor-head collar for rigidity +COL_BOLT_X = 19.0; // M4 bolt CL from stem axis +COL_BOLT_D = 4.5; // M4 clearance +THUMB_HEAD_D= 8.0; // M4 thumbscrew head OD (slot for access) +COL_NUT_W = 7.0; // M4 hex nut A/F +COL_NUT_H = 3.4; + +// Anti-rotation flat tab: a 3 mm wall tab that protrudes radially +// and bears against the bracket arm, preventing axial rotation +// without needing a stem flat. +ANTI_ROT_T = 3.0; // tab thickness (radial) +ANTI_ROT_W = 8.0; // tab width (tangential) +ANTI_ROT_Z = 4.0; // distance from collar base + +// USB cable channel: groove on collar outer surface, runs Z direction +// Cable routes from anchor module down to base +USB_CHAN_W = 9.0; // channel width (fits USB-A cable Ø6 mm) +USB_CHAN_D = 5.0; // channel depth + +// ── Module bracket ─────────────────────────────────────────── +ARM_L = 20.0; // arm length from collar OD to bracket face +ARM_W = MAWB_W + 6.0; // bracket width (Y, includes side walls) +ARM_H = 6.0; // arm thickness (Z) +BRKT_TILT = 10.0; // tilt outward from vertical (antenna faces horizon) + +BRKT_BACK_T = 3.0; // bracket back wall (module sits against this) +BRKT_SIDE_T = 2.0; // bracket side walls + +M2_STNDFF = 3.0; // M2 standoff height +M2_STNDFF_OD= 4.5; + +// USB port access notch in bracket side wall (8×5 mm) +USB_NOTCH_W = 10.0; +USB_NOTCH_H = 7.0; + +// ── Spacing ─────────────────────────────────────────────────── +ANCHOR_SPACING = 250.0; // centre-to-centre Z separation + +$fn = 64; +e = 0.01; + +// ───────────────────────────────────────────────────────────── +// collar_half(side) +// split at Y=0 plane. Bracket arm on front (+Y) half. +// Print flat-face-down. +// ───────────────────────────────────────────────────────────── +module collar_half(side = "front") { + y_front = (side == "front"); + + difference() { + union() { + // D-shaped body + intersection() { + cylinder(d=COL_OD, h=COL_H); + translate([-COL_OD/2, y_front ? 0 : -COL_OD/2, 0]) + cube([COL_OD, COL_OD/2, COL_H]); + } + + // Anti-rotation tab (front half only, at +X side) + if (y_front) { + translate([COL_OD/2, -ANTI_ROT_W/2, ANTI_ROT_Z]) + cube([ANTI_ROT_T, ANTI_ROT_W, + COL_H - ANTI_ROT_Z - 4]); + } + + // Bracket arm attachment boss (front half only, top centre) + if (y_front) { + translate([-ARM_W/2, COL_OD/2, COL_H * 0.3]) + cube([ARM_W, ARM_L, COL_H * 0.4]); + } + } + + // ── Stem bore ───────────────────────────────────────── + translate([0,0,-e]) + cylinder(d=STEM_BORE, h=COL_H + 2*e); + + // ── M4 clamping bolt holes (Y direction) ────────────── + for (bx=[-COL_BOLT_X, COL_BOLT_X]) { + translate([bx, y_front ? COL_OD/2 : 0, COL_H/2]) + rotate([90,0,0]) + cylinder(d=COL_BOLT_D, h=COL_OD/2 + e); + // Thumbscrew head recess on outer face (front only — access side) + if (y_front) { + translate([bx, COL_OD/2 - WALL, COL_H/2]) + rotate([90,0,0]) + cylinder(d=THUMB_HEAD_D, h=8 + e); + } + } + + // ── M4 hex nut pockets (rear half) ──────────────────── + if (!y_front) { + for (bx=[-COL_BOLT_X, COL_BOLT_X]) { + translate([bx, -(COL_OD/4 + e), COL_H/2]) + rotate([90,0,0]) + cylinder(d=COL_NUT_W/cos(30), h=COL_NUT_H + e, + $fn=6); + } + } + + // ── Set screw (height lock, front half) ─────────────── + if (y_front) { + translate([0, COL_OD/2, COL_H * 0.8]) + rotate([90,0,0]) + cylinder(d=COL_BOLT_D, + h=COL_OD/2 - STEM_BORE/2 + e); + } + + // ── USB cable routing channel (rear half, −X side) ──── + if (!y_front) { + translate([-COL_OD/2, -USB_CHAN_W/2, -e]) + cube([USB_CHAN_D, USB_CHAN_W, COL_H + 2*e]); + } + + // ── M4 hole through arm boss (Z direction, for bracket bolt) ─ + if (y_front) { + for (dx=[-ARM_W/4, ARM_W/4]) + translate([dx, COL_OD/2 + ARM_L/2, COL_H * 0.35]) + cylinder(d=COL_BOLT_D, h=COL_H * 0.35 + e); + } + } +} + +// ───────────────────────────────────────────────────────────── +// module_bracket() +// Bolts to collar arm boss. Holds MaUWB PCB facing outward. +// Tilted BRKT_TILT° from vertical — antenna clears stem. +// Print flat-face-down (back wall on bed). +// ───────────────────────────────────────────────────────────── +module module_bracket() { + bk = BRKT_BACK_T; + sd = BRKT_SIDE_T; + + difference() { + union() { + // ── Back wall (mounts to collar arm boss) ───────── + cube([ARM_W, bk, MAWB_H + M2_STNDFF + 6]); + + // ── Side walls ──────────────────────────────────── + for (sx=[0, ARM_W - sd]) + translate([sx, bk, 0]) + cube([sd, MAWB_L + 2, MAWB_H + M2_STNDFF + 6]); + + // ── M2 standoff posts (PCB mounts to these) ─────── + for (hx=[0, MAWB_HOLE_X], hy=[0, MAWB_HOLE_Y]) + translate([(ARM_W - MAWB_HOLE_X)/2 + hx, + bk + (MAWB_L - MAWB_HOLE_Y)/2 + hy, + 0]) + cylinder(d=M2_STNDFF_OD, h=M2_STNDFF); + } + + // ── M2 bores through standoffs ──────────────────────── + for (hx=[0, MAWB_HOLE_X], hy=[0, MAWB_HOLE_Y]) + translate([(ARM_W - MAWB_HOLE_X)/2 + hx, + bk + (MAWB_L - MAWB_HOLE_Y)/2 + hy, + -e]) + cylinder(d=M2_D, h=M2_STNDFF + e); + + // ── Antenna clearance cutout in back wall ───────────── + // Open slot near top of back wall so antenna is unobstructed + translate([sd, -e, M2_STNDFF + 2]) + cube([ARM_W - 2*sd, bk + 2*e, MAWB_H]); + + // ── USB port access notch on one side wall ──────────── + translate([-e, bk + 2, M2_STNDFF - 1]) + cube([sd + 2*e, USB_NOTCH_W, USB_NOTCH_H]); + + // ── Mounting holes to collar arm boss (×2) ──────────── + for (dx=[-ARM_W/4, ARM_W/4]) + translate([ARM_W/2 + dx, bk + ARM_L/2, -e]) + cylinder(d=COL_BOLT_D, h=6 + e); + } +} + +// ───────────────────────────────────────────────────────────── +// single_anchor_assembly() +// ───────────────────────────────────────────────────────────── +module single_anchor_assembly(show_phantom=false) { + // Collar + color("SteelBlue", 0.9) collar_half("front"); + color("CornflowerBlue", 0.9) mirror([0,1,0]) collar_half("rear"); + + // Bracket tilted BRKT_TILT° outward from top of arm boss + color("LightSteelBlue", 0.85) + translate([0, COL_OD/2 + ARM_L, COL_H * 0.3]) + rotate([BRKT_TILT, 0, 0]) + translate([-ARM_W/2, 0, 0]) + module_bracket(); + + // Phantom UWB PCB + if (show_phantom) + color("ForestGreen", 0.4) + translate([-MAWB_L/2, + COL_OD/2 + ARM_L + BRKT_BACK_T, + COL_H * 0.3 + M2_STNDFF]) + cube([MAWB_L, MAWB_W, MAWB_H]); +} + +// ───────────────────────────────────────────────────────────── +// Render selector +// ───────────────────────────────────────────────────────────── +if (RENDER == "assembly") { + single_anchor_assembly(show_phantom=true); + +} else if (RENDER == "collar_front") { + collar_half("front"); + +} else if (RENDER == "collar_rear") { + collar_half("rear"); + +} else if (RENDER == "bracket") { + module_bracket(); + +} else if (RENDER == "pair") { + // Both anchors at 250 mm spacing on a stem stub + color("Silver", 0.2) + translate([0, 0, -50]) + cylinder(d=STEM_OD, h=ANCHOR_SPACING + COL_H + 100); + + // Lower anchor (Z = 0) + single_anchor_assembly(show_phantom=true); + + // Upper anchor (Z = ANCHOR_SPACING) + translate([0, 0, ANCHOR_SPACING]) + single_anchor_assembly(show_phantom=true); +} + diff --git a/chassis/uwb_assembly.md b/chassis/uwb_assembly.md new file mode 100644 index 0000000..3c8cfa2 --- /dev/null +++ b/chassis/uwb_assembly.md @@ -0,0 +1,204 @@ +# SaltyBot UWB System — Assembly Notes + BOM +**Rev A — 2026-03-01 — sl-mechanical** +**Issues: #57, #61, #62** + +--- + +## System Overview + +Follow-me UWB ranging using 3× MaUWB ESP32-S3 modules: + +| Role | Qty | Location | File | +|------|-----|----------|------| +| Tag | 1 | Operator belt ("Tee") | `uwb_tag_enclosure.scad` | +| Anchor | 2 | Robot stem, 250 mm apart | `uwb_anchor_mount.scad` | + +--- + +## Antenna Clearance — CRITICAL + +The DW3000 UWB chip antenna requires: +- **10 mm clear zone** around antenna — no metal, no carbon fibre, no metal-fill filament +- Use **plain PLA or PETG** only in these parts (no conductive additives) +- Tag lid has an **open window** directly over the PCB antenna area +- Anchor bracket **back wall has a cutout** behind the antenna section of the PCB +- When installing modules, orient antenna end **away from stem** (faces outward) + +--- + +## Part A — UWB Tag Enclosure (`uwb_tag_enclosure.scad`) + +### Printed parts + +| # | RENDER | Qty | Material | Settings | +|---|--------|-----|----------|----------| +| 1 | `body` | 1 | PETG | 4 perims, 35% infill, 0.2 mm layer | +| 2 | `lid` | 1 | PETG | 4 perims, 35% infill | +| 3 | `tpu_bumper` | 1 | TPU 95A | 3 perims, 20% infill, 25 mm/s | + +> **Print orientation:** body — floor on bed; lid — inside face down; TPU bumper — flat. + +### Internal layout diagram + +``` +X− ←────────── ~75 mm ──────────→ X+ + ┌──────────────────────────────┐ + USB → │[TP4056] [ 18650 cell ] │ + │─────────────────────────────│ + │ [ MaUWB ESP32-S3 PCB ] │ ← antenna faces UP + └──────────────────────────────┘ + ↑ ↑ + micro-USB LED window + (charging) (Y+ face, small hole) + + Power switch cutout: Y− face, centred + Belt clip: Y− face (back) +``` + +### IP44 seam + +Lid rim overlaps body 4 mm with a 2-turn labyrinth ridge (water must turn +90° twice). No holes on the top face. All ports on side/back faces. +**Not suitable for rain immersion — use for light splash protection only.** + +### Heat-set insert option + +For improved thread strength, replace M2 clearance standoffs with **M2 × 3 mm +heat-set brass inserts** pressed into the standoff posts with a soldering iron. + +### Fasteners + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| 4 | M2 × 6 BHCS | 4 | MaUWB PCB to standoffs | +| 5 | M2 hex nut | 4 | Alternative if no heat-set inserts | +| 6 | M3 × 8 SHCS | 2 | Belt clip to enclosure back | +| 7 | M3 hex nut | 2 | Captured in clip back plate | + +### Assembly sequence — tag + +1. Install TP4056 board in body, micro-USB port aligned with cutout +2. Solder TP4056 leads to 18650 spring contacts (or use 18650 holder PCB) +3. Snap 18650 into battery cradle (+ end toward spring contact at X+) +4. Seat MaUWB PCB on M2 standoffs; tighten M2 × 6 bolts (0.3 N·m — soft) +5. Route USB data + power cable from MaUWB to TP4056 B+ / B− pads +6. Press power switch into cutout (Y− face) +7. Close lid — press corners until 4 snap clips engage (audible click ×4) +8. Slide TPU bumper sleeve onto body from below +9. Attach belt clip to Y− face with 2× M3 × 8 + +--- + +## Part B — Anchor Mounts (`uwb_anchor_mount.scad`) + +### Printed parts (per mount × 2 mounts) + +| # | RENDER | Qty per mount | Total | Material | Notes | +|---|--------|---------------|-------|----------|-------| +| 10 | `collar_front` | 1 | 2 | PETG | Flat-face-down; has thumbscrew recesses | +| 11 | `collar_rear` | 1 | 2 | PETG | Has hex nut pockets + USB cable channel | +| 12 | `bracket` | 1 | 2 | PETG | Flat-face-down; back wall on bed | + +### Anti-rotation + +The front collar half has a **flat tab** (3 × 8 mm radial tab at +X) that +protrudes from the collar outer surface. This tab: +- Bears against the module bracket arm, preventing rotation during clamping +- Creates a positive mechanical register — mount always faces the same direction +- No modification to the stem required + +### Anchor positioning on stem + +``` +TOP OF STEM + │ + ──┼── ← Sensor head (sensor_head.scad, at ~800–900 mm) + │ + ══╬══ ← Upper anchor (ANCHOR 2) ┐ + │ │ 250 mm + ══╬══ ← Lower anchor (ANCHOR 1) ┘ + │ + ──┼── ← Battery carousel (stem_battery_clamp.scad) + │ +BOTTOM OF STEM (base plate) +``` + +Recommended Z positions (above base plate): +- Anchor 1: **450 mm** (just above battery carousel top) +- Anchor 2: **700 mm** + +This places anchors in the mid-stem region, maximising horizontal separation +from each other and from nearby metal (battery packs, base plate). + +### USB cable routing + +Each anchor is powered via USB-A from the Jetson Orin. Cable routing: +1. Cable exits Orin USB hub, runs up stem through cable ties +2. Enters anchor bracket through USB notch on left side wall +3. Plugs into MaUWB USB-C port (programming and power) +4. Excess cable loops into rear collar USB channel groove, held with a cable tie + +### Fasteners (per mount) + +| # | Spec | Qty | Use | +|---|------|-----|-----| +| 13 | M4 × 25 SHCS (thumbscrew head) | 2 | Collar clamping bolts (tool-free: slot for coin) | +| 14 | M4 hex nut | 2 | Captured in rear collar half | +| 15 | M4 × 10 set screw | 1 | Collar height lock (front half) | +| 16 | M4 × 20 SHCS | 2 | Bracket to collar arm boss | +| 17 | M4 hex nut | 2 | Under collar boss for bracket bolts | +| 18 | M2 × 6 BHCS | 4 | MaUWB PCB to bracket standoffs | +| 19 | M2 hex nut | 4 | PCB standoff nuts | + +### Assembly sequence — anchor (×2) + +1. Press M4 hex nuts into collar rear half nut pockets +2. Press M2 hex nuts into bracket standoff counterbores +3. Seat MaUWB PCB in bracket on M2 standoffs; tighten M2 × 6 BHCS (0.3 N·m) +4. Bolt bracket to collar front-half arm boss with 2× M4 × 20 +5. Align anti-rotation tab with bracket arm edge +6. Wrap collar halves around stem at desired height +7. Align micro-USB / USB-C port direction, tighten M4 thumbscrews finger-tight +8. Tighten set screw on front half to lock height +9. Route USB cable through rear channel, cable-tie every ~100 mm along stem + +--- + +## Full Mass Estimate + +| Part | Material | Est. mass | +|------|----------|-----------| +| Tag body + lid (PETG) | PETG | ~45 g | +| Tag TPU bumper | TPU | ~12 g | +| 18650 cell | Li-ion | ~47 g | +| TP4056 + MaUWB PCBs | PCB | ~20 g | +| Tag total | | **~130 g** | +| Anchor collar set × 2 (×2) | PETG | ~80 g | +| Anchor brackets × 2 | PETG | ~40 g | +| MaUWB PCBs × 2 | PCB | ~30 g | +| Anchor fasteners | SS | ~20 g | +| **Total (tag + 2 anchors)** | | **~300 g** | + +--- + +## Bill of Materials Summary + +| # | Description | Qty | Source hint | +|---|-------------|-----|-------------| +| A | MaUWB ESP32-S3 module | 3 | AliExpress / MakerFabs | +| B | 18650 Li-ion cell (≥2000 mAh) | 1 | Panasonic NCR18650B or equiv. | +| C | TP4056 micro-USB charger board | 1 | Standard TP4056 module | +| D | Slide power switch 13×7 mm | 1 | SS-12D00 or equiv. | +| E | M2 × 6 BHCS | 12 | (4 tag + 4 anchor-1 + 4 anchor-2) | +| F | M2 hex nut | 12 | | +| G | M3 × 8 SHCS | 2 | Belt clip | +| H | M3 hex nut | 2 | | +| I | M4 × 25 thumbscrew SHCS | 4 | Slot-head preferred for tool-free use | +| J | M4 × 20 SHCS | 4 | Bracket-to-collar | +| K | M4 × 10 set screw | 2 | Height lock | +| L | M4 hex nut | 8 | | +| M | USB-A to USB-C cable 1 m | 2 | Anchor power from Orin | +| N | PETG filament | ~200 g | Any brand | +| O | TPU 95A filament | ~30 g | For tag bumper | +| P | Loctite 243 blue | 1 | Structural M4 bolts | +| Q | Cable ties 100 mm | 10 | Stem cable management | diff --git a/chassis/uwb_tag_enclosure.scad b/chassis/uwb_tag_enclosure.scad new file mode 100644 index 0000000..fa1dc68 --- /dev/null +++ b/chassis/uwb_tag_enclosure.scad @@ -0,0 +1,369 @@ +// ============================================================ +// uwb_tag_enclosure.scad — Wearable UWB Tag Rev B 2026-03-01 +// Agent: sl-mechanical +// Closes issues #57, #61 +// ============================================================ +// Belt-clip enclosure for "follow-me" UWB tag worn by operator. +// +// Components: +// MaUWB ESP32-S3 module ~50 × 25 × 10 mm PCB +// TP4056 charger board ~25 × 19 mm, micro-USB port +// 18650 Li-ion cell Ø18 × 65 mm +// +// ⚠ VERIFY WITH CALIPERS BEFORE PRINTING: +// MAWB_L, MAWB_W, MAWB_H — UWB module +// TP_L, TP_W — TP4056 board +// TP_USB_W, TP_USB_H — micro-USB receptacle +// SW_W, SW_H — power switch body +// +// Key design decisions: +// • UWB antenna window (open rectangle) in LID — no material +// within 10 mm of antenna area; antenna faces up +// • IP44-ish: lid overlaps body 4 mm + 2-turn labyrinth seam; +// all ports on side faces, NO holes in top face +// • TPU bumper sleeve snaps around body bottom edge (drop protection) +// • Micro-USB on short face (X−), power switch on long face (Y−) +// • Belt clip integrated on back face (Y−) +// +// Internal layout (cross-section view from top): +// +// Y+ ┌──────────────────────────────────────────┐ +// │ [ MaUWB PCB 50×25mm flat ] │ +// │ [TP4056] ──── [ 18650 cell ] ────── │ +// Y− └──────────────────────────────────────────┘ +// X− X+ +// +// RENDER options: +// "assembly" closed box (default) +// "body" lower shell for slicing +// "lid" snap-fit lid for slicing +// "tpu_bumper" TPU edge sleeve for slicing (print in TPU 95A) +// "body_2d" bottom projection → DXF +// ============================================================ + +RENDER = "assembly"; + +// ── ⚠ Verify-before-print ──────────────────────────────────── +// MaUWB ESP32-S3 +MAWB_L = 50.0; // PCB length (X) +MAWB_W = 25.0; // PCB width (Y) +MAWB_H = 10.0; // PCB + tallest component (Z) +MAWB_HOLE_X = 43.0; // M2 mounting hole spacing X (verify) +MAWB_HOLE_Y = 20.0; // M2 mounting hole spacing Y (verify) +M2_D = 2.2; // M2 clearance +M2_STNDFF = 3.0; // standoff height (PCB proud of tray floor) +M2_STNDFF_OD= 4.5; // standoff post OD + +// TP4056 charger +TP_L = 25.0; // PCB length (X, at X− end) +TP_W = 19.0; // PCB width (Y) +TP_H = 5.0; // PCB + components (Z) +TP_USB_W = 8.0; // micro-USB receptacle width +TP_USB_H = 3.5; // micro-USB receptacle height + +// Power switch (slide or toggle, 13×7 mm body) +SW_W = 13.0; // switch body length (X) +SW_H = 7.0; // switch body height (Z) +SW_BEZEL_W = 8.0; // switch cutout width in wall +SW_BEZEL_H = 5.0; // switch cutout height in wall +SW_Y_POS = 6.0; // distance from bottom of internal space (Z) + +// 18650 cell +BATT_OD = 18.6; // diameter + clearance +BATT_L = 65.5; // length + clearance + +// ── Enclosure shell ────────────────────────────────────────── +WALL = 2.2; // shell wall thickness +FLOOR = 2.5; // floor thickness (bottom face printed) +LID_H = 3.0; // lid panel thickness +LID_OVLP = 4.0; // lid rim overlap depth (IP44 seal) +LID_LBYRTH = 1.0; // labyrinth ridge height +LID_LBYRTH_W= 1.2; // labyrinth ridge width + +// Snap clips (×4, one per side) +SNAP_W = 8.0; +SNAP_T = 1.5; +SNAP_H = 1.8; + +// ── Internal component layout ──────────────────────────────── +// Battery runs in X at Y = INT_W/2 − BATT_OD/2 (left/bottom of bay) +// TP4056 at X− end, beside battery in Y +// MaUWB PCB flat above battery and TP4056 (stacked) + +// Z stack: +// Z=0 body floor +// Z=FLOOR interior floor +// Z=FLOOR..FLOOR+BATT_OD battery (18 mm dia) +// Z=FLOOR+BATT_OD+1 PCB tray floor (1 mm above battery) +// Z=PCB_TRAY+M2_STNDFF PCB bottom face +// Z=PCB_TRAY+M2_STNDFF+MAWB_H PCB top +// + 2 mm clearance + LID +PCB_TRAY_Z = FLOOR + BATT_OD + 1.5; +PCB_BOT_Z = PCB_TRAY_Z + M2_STNDFF; +PCB_TOP_Z = PCB_BOT_Z + MAWB_H; + +// Internal dimensions +INT_L = BATT_L + 5.0; // 70.5 mm (battery + wiring room) +INT_W = BATT_OD + MAWB_W - 10; // ~33 mm (battery + PCB overlap in Y) +INT_H = PCB_TOP_Z - FLOOR + 2.5; // ~31 mm + +// External dimensions +EXT_L = INT_L + 2 * WALL; +EXT_W = INT_W + 2 * WALL; +EXT_H = INT_H + FLOOR; + +// ── Belt clip ───────────────────────────────────────────────── +CLIP_H = 58.0; // total clip height +CLIP_GAP = 5.5; // belt slot gap (fits 5 mm thick belt) +CLIP_BELT_W = 42.0; // belt slot width +CLIP_BACK_T = 4.0; // clip back-plate thickness +CLIP_FLEX_T = 2.0; // spring arm thickness + +// ── TPU bumper sleeve ───────────────────────────────────────── +TPU_T = 4.0; // bumper wall thickness +TPU_H = 12.0; // bumper height (covers bottom edge) +TPU_CR = 5.0; // outer corner radius + +$fn = 48; +e = 0.01; + +// ───────────────────────────────────────────────────────────── +module rounded_box(l, w, h, r = 3.5) { + hull() + for (x=[r, l-r], y=[r, w-r]) + translate([x, y, 0]) + cylinder(r=r, h=h); +} + +// ───────────────────────────────────────────────────────────── +// enclosure_body() Print: floor on build plate, no supports. +// ───────────────────────────────────────────────────────────── +module enclosure_body() { + difference() { + union() { + // ── Outer shell ────────────────────────────────── + rounded_box(EXT_L, EXT_W, EXT_H, r = 3.5); + + // ── IP44 labyrinth ridge on top rim ────────────── + // Inner ridge that mates with lid groove + translate([WALL - LID_LBYRTH_W, WALL - LID_LBYRTH_W, + EXT_H - LID_LBYRTH]) + difference() { + rounded_box(INT_L + 2*LID_LBYRTH_W, + INT_W + 2*LID_LBYRTH_W, + LID_LBYRTH + e, r = 2.5); + translate([LID_LBYRTH_W, LID_LBYRTH_W, -e]) + cube([INT_L, INT_W, LID_LBYRTH + 2*e]); + } + + // ── M2 standoffs for UWB PCB ───────────────────── + bx = WALL + (INT_L - MAWB_L)/2; + by = WALL + (INT_W - MAWB_W)/2; + for (sx=[0, MAWB_HOLE_X], sy=[0, MAWB_HOLE_Y]) + translate([bx + (MAWB_L - MAWB_HOLE_X)/2 + sx, + by + (MAWB_W - MAWB_HOLE_Y)/2 + sy, + 0]) + cylinder(d=M2_STNDFF_OD, h=PCB_TRAY_Z + M2_STNDFF); + } + + // ── Interior cavity ─────────────────────────────────── + translate([WALL, WALL, FLOOR]) + cube([INT_L, INT_W, INT_H + e]); + + // ── 18650 battery cradle ────────────────────────────── + // Semi-cylindrical channel in floor, Y− side of interior + hull() { + translate([WALL + 3, WALL + BATT_OD/2, FLOOR]) + rotate([-90, 0, 90]) + cylinder(d=BATT_OD, h=BATT_L - 3); + } + // Positive end spring hole + translate([WALL + INT_L - 2, WALL + BATT_OD/2, FLOOR]) + cylinder(d=8, h=FLOOR + e); + + // ── Micro-USB cutout (X− face) ──────────────────────── + translate([-e, WALL + (INT_W - TP_USB_W)/2, + FLOOR + BATT_OD/2 - TP_USB_H/2 + 1]) + cube([WALL + 2*e, TP_USB_W, TP_USB_H]); + + // ── Power switch cutout (Y− face) ───────────────────── + translate([WALL + (INT_L - SW_W)/2, -e, + FLOOR + SW_Y_POS]) + cube([SW_BEZEL_W, WALL + 2*e, SW_BEZEL_H]); + + // ── Status LED window (Y+ face, small hole) ─────────── + // TP4056 charge LED and ESP32 LED visible from side + translate([WALL + TP_L / 2, EXT_W - WALL - e, + FLOOR + BATT_OD + 2]) + rotate([90, 0, 0]) + cylinder(d=2.5, h=WALL + 2*e); + + // ── M2 standoff bores ───────────────────────────────── + bx = WALL + (INT_L - MAWB_L)/2; + by = WALL + (INT_W - MAWB_W)/2; + for (sx=[0, MAWB_HOLE_X], sy=[0, MAWB_HOLE_Y]) + translate([bx + (MAWB_L - MAWB_HOLE_X)/2 + sx, + by + (MAWB_W - MAWB_HOLE_Y)/2 + sy, + -e]) + cylinder(d=M2_D, h=PCB_TRAY_Z + M2_STNDFF + e); + + // ── Lid snap recesses on body exterior rim ──────────── + // Front and rear pair + for (ys=[WALL/2, EXT_W - WALL/2 - SNAP_T]) { + translate([EXT_L/2 - SNAP_W/2, ys, + EXT_H - SNAP_H - 0.2]) + cube([SNAP_W, SNAP_T + e, SNAP_H + 0.2]); + } + // Left and right pair + for (xs=[WALL/2, EXT_L - WALL/2 - SNAP_T]) { + translate([xs, EXT_W/2 - SNAP_W/2, + EXT_H - SNAP_H - 0.2]) + cube([SNAP_T + e, SNAP_W, SNAP_H + 0.2]); + } + } +} + +// ───────────────────────────────────────────────────────────── +// enclosure_lid() Print: inside face down. +// ───────────────────────────────────────────────────────────── +module enclosure_lid() { + tol = 0.3; + lid_l = EXT_L + tol; + lid_w = EXT_W + tol; + + difference() { + union() { + // Lid panel + rounded_box(lid_l, lid_w, LID_H, r = 3.5); + + // Rim that drops over body (IP44 labyrinth) + translate([0, 0, -LID_OVLP]) + difference() { + rounded_box(lid_l, lid_w, LID_OVLP, r = 3.5); + // Hollow inside rim + translate([WALL + tol/2, WALL + tol/2, -e]) + cube([INT_L - tol, INT_W - tol, LID_OVLP + 2*e]); + } + + // Labyrinth groove ridge (mates with body ridge) + translate([WALL - LID_LBYRTH_W + tol/2, + WALL - LID_LBYRTH_W + tol/2, + -LID_OVLP + LID_LBYRTH_H/2]) + difference() { + rounded_box(INT_L + 2*LID_LBYRTH_W - tol, + INT_W + 2*LID_LBYRTH_W - tol, + LID_LBYRTH + 0.5, r = 2.5); + translate([LID_LBYRTH_W, LID_LBYRTH_W, -e]) + cube([INT_L - tol, INT_W - tol, + LID_LBYRTH + 0.7]); + } + } + + // ── UWB antenna window (NO lid material over antenna) ─ + // PCB centred in enclosure → antenna window centred in lid + bx = (lid_l - MAWB_L + 4) / 2; + by = (lid_w - MAWB_W + 4) / 2; + translate([bx, by, -e]) + rounded_box(MAWB_L - 4, MAWB_W - 4, + LID_H + 2*e, r = 2); + + // ── Snap tabs on inside of lid rim ──────────────────── + for (ys=[WALL/2, lid_w - WALL/2 - SNAP_T]) { + translate([lid_l/2 - SNAP_W/2, ys, + -LID_OVLP + (LID_OVLP - SNAP_H)/2]) + cube([SNAP_W, SNAP_T + e, SNAP_H]); + } + for (xs=[WALL/2, lid_l - WALL/2 - SNAP_T]) { + translate([xs, lid_w/2 - SNAP_W/2, + -LID_OVLP + (LID_OVLP - SNAP_H)/2]) + cube([SNAP_T + e, SNAP_W, SNAP_H]); + } + } +} + +// ───────────────────────────────────────────────────────────── +// belt_clip() Print: back face down. Use PETG for flex. +// ───────────────────────────────────────────────────────────── +module belt_clip() { + difference() { + union() { + // Back plate (mounts flush to Y− face of enclosure) + cube([EXT_L, CLIP_BACK_T, CLIP_H]); + + // Outer spine + translate([0, CLIP_BACK_T, 0]) + cube([EXT_L, CLIP_FLEX_T + CLIP_GAP, CLIP_H]); + + // Top bridge + translate([0, CLIP_BACK_T, CLIP_H - CLIP_FLEX_T]) + cube([EXT_L, CLIP_GAP + CLIP_FLEX_T + CLIP_BACK_T, + CLIP_FLEX_T]); + + // Flex spring arm (tapers for compliance) + translate([EXT_L * 0.1, CLIP_BACK_T + CLIP_GAP, 0]) + hull() { + cube([EXT_L * 0.8, CLIP_FLEX_T, 1]); + translate([EXT_L * 0.05, 1, CLIP_H - CLIP_FLEX_T - 4]) + cube([EXT_L * 0.7, CLIP_FLEX_T + 1, 1]); + } + } + + // Belt slot through outer spine + translate([(EXT_L - CLIP_BELT_W) / 2, CLIP_BACK_T - e, + CLIP_FLEX_T + 3]) + cube([CLIP_BELT_W, CLIP_GAP + CLIP_FLEX_T + 2*e, + CLIP_H - 2*CLIP_FLEX_T - 6]); + + // 2× M3 screws (clip → enclosure back face) + for (x = [EXT_L*0.25, EXT_L*0.75]) + translate([x, -e, CLIP_H/2]) + rotate([-90, 0, 0]) + cylinder(d=3.3, h=CLIP_BACK_T + 2*e); + } +} + +// ───────────────────────────────────────────────────────────── +// tpu_bumper() Print in TPU 95A. Snaps around body bottom. +// ───────────────────────────────────────────────────────────── +module tpu_bumper() { + // Outer frame; inner cutout = body exterior + 0.5 clearance + cl = 0.5; + difference() { + // Outer rounded rectangle + translate([-TPU_T, -TPU_T, 0]) + rounded_box(EXT_L + 2*TPU_T, EXT_W + 2*TPU_T, + TPU_H, r = TPU_CR); + + // Inner cutout (fits over body) + translate([-cl/2, -cl/2, -e]) + rounded_box(EXT_L + cl, EXT_W + cl, + TPU_H + 2*e, r = 3.5); + } +} + +// ───────────────────────────────────────────────────────────── +// Render selector +// ───────────────────────────────────────────────────────────── +if (RENDER == "assembly") { + color("SlateGray", 0.92) enclosure_body(); + color("DimGray", 0.75) translate([0, 0, EXT_H]) + enclosure_lid(); + color("SteelBlue", 0.85) translate([0, -CLIP_BACK_T, 0]) + belt_clip(); + color("OrangeRed", 0.65) translate([0, 0, -TPU_H - 0.5]) + tpu_bumper(); + +} else if (RENDER == "body") { + enclosure_body(); + +} else if (RENDER == "lid") { + enclosure_lid(); + +} else if (RENDER == "tpu_bumper") { + tpu_bumper(); + +} else if (RENDER == "body_2d") { + projection(cut = true) + translate([0, 0, -FLOOR/2]) + enclosure_body(); +}