feat: UWB tag enclosure + anchor mounts (#57, #61, #62) #64

Merged
seb merged 1 commits from sl-mechanical/uwb-enclosures into main 2026-03-01 00:51:12 -05:00
3 changed files with 848 additions and 0 deletions

View File

@ -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);
}

204
chassis/uwb_assembly.md Normal file
View File

@ -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 ~800900 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 |

View File

@ -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();
}