saltylab-firmware/chassis/uwb_anchor_mount.scad

484 lines
24 KiB
OpenSCAD
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// uwb_anchor_mount.scad — Wall/Ceiling UWB Anchor Mount Bracket
// Issue: #564 Agent: sl-mechanical Date: 2026-03-14
// (supersedes Rev A stem-collar mount — see git history)
// ============================================================
//
// Parametric wall or ceiling mount bracket for ESP32 UWB Pro anchor.
// Designed for fixed-infrastructure deployment: anchors screw into
// wall or ceiling drywall/timber with standard M4 or #6 wood screws,
// at a user-defined tilt angle so the UWB antenna faces the desired
// coverage zone.
//
// Architecture:
// Wall base → flat backplate with 2× screw holes (wall or ceiling)
// Tilt knuckle → single-axis articulating joint; TILT_DEG steps (15°)
// locked with M3 bolt+nut; range 090° (wall to ceiling)
// Anchor cradle→ U-cradle holding ESP32 UWB Pro PCB on M2.5 standoffs
// USB-C channel→ routed exit groove on cradle side
// Label slot → rear window slot for printed anchor-ID label card
//
// Part catalogue:
// Part 1 — wall_base() Backplate + 2-ear pivot block
// Part 2 — tilt_arm() Pivoting arm; knuckle + cradle arm
// Part 3 — anchor_cradle() PCB cradle with standoffs + USB-C slot + label window
// Part 4 — cable_clip() Snap-on USB-C cable guide for tilt arm
// Part 5 — assembly_preview()
//
// Hardware BOM:
// 2× M4 × 30 mm wood screws (or #6 drywall screws) wall fasteners
// 1× M3 × 20 mm SHCS + M3 nyloc nut tilt pivot bolt
// 4× M2.5 × 8 mm SHCS PCB-to-cradle
// 4× M2.5 hex nuts captured in standoffs
// 1× USB-C cable anchor power
//
// ESP32 UWB Pro interface (⚠ verify with calipers):
// PCB size : UWB_L × UWB_W × UWB_H (55 × 28 × 10 mm default)
// Mounting holes : M2.5, 4× corners on UWB_HOLE_X × UWB_HOLE_Y pattern
// USB-C port : centred on short edge (X face), UWB_USBC_W × UWB_USBC_H
// Antenna area : top face, rear half — 10 mm keep-out of bracket material
//
// Tilt angles available (15° detent steps, TILT_DEG = 090):
// 0° → horizontal face-up (ceiling mount, antenna faces down)
// 15° → slight downward tilt (ceiling corner)
// 30° → downward 30° (wall near ceiling)
// 45° → 45° diagonal (wall mid-height)
// 60° → near-vertical (wall, antenna faces across room)
// 75° → 75° from horizontal
// 90° → vertical face-out (wall mount, antenna faces forward)
//
// RENDER options:
// "assembly" full assembly at TILT_DEG (default)
// "wall_base_stl" Part 1
// "tilt_arm_stl" Part 2
// "anchor_cradle_stl" Part 3
// "cable_clip_stl" Part 4
//
// Export commands:
// openscad uwb_anchor_mount.scad -D 'RENDER="wall_base_stl"' -o uwb_wall_base.stl
// openscad uwb_anchor_mount.scad -D 'RENDER="tilt_arm_stl"' -o uwb_tilt_arm.stl
// openscad uwb_anchor_mount.scad -D 'RENDER="anchor_cradle_stl"' -o uwb_anchor_cradle.stl
// openscad uwb_anchor_mount.scad -D 'RENDER="cable_clip_stl"' -o uwb_cable_clip.stl
// ============================================================
$fn = 64;
e = 0.01;
// ── Tilt angle (override per anchor, 090°, 15° steps) ───────────────────────
TILT_DEG = 30; // default: 30° downward tilt from horizontal
// ── ESP32 UWB Pro PCB dimensions (verify with calipers) ──────────────────────
UWB_L = 55.0; // PCB length (Y axis in cradle)
UWB_W = 28.0; // PCB width (X axis in cradle)
UWB_H = 10.0; // PCB + components height (Z in cradle)
UWB_HOLE_X = 47.5; // M2.5 hole X span (centre-to-centre)
UWB_HOLE_Y = 21.0; // M2.5 hole Y span
UWB_USBC_W = 9.5; // USB-C receptacle width
UWB_USBC_H = 4.0; // USB-C receptacle height
UWB_ANTENNA_L = 20.0; // antenna area length at PCB rear (keep-out zone)
// ── Wall base geometry ────────────────────────────────────────────────────────
BASE_W = 60.0; // backplate width (X)
BASE_H = 50.0; // backplate height (Z) — "height" when on wall
BASE_T = 5.0; // backplate thickness (Y, into wall)
BASE_SCREW_D = 4.5; // M4 / #6 screw clearance bore
BASE_SCREW_HD = 8.5; // screw head countersink diameter
BASE_SCREW_HH = 3.5; // countersink depth
BASE_SCREW_SPC = 35.0; // screw hole centre-to-centre (Z span)
KNUCKLE_W = 14.0; // pivot block width (X span between ears)
KNUCKLE_T = BASE_T + 4.0; // pivot block Y depth (proud of base face)
// ── Tilt arm geometry ─────────────────────────────────────────────────────────
ARM_W = 12.0; // arm width (X)
ARM_T = 5.0; // arm thickness (Y)
ARM_L = 35.0; // arm length (distance from pivot to cradle attach)
PIVOT_D = 3.3; // M3 pivot bolt clearance
PIVOT_NUT_AF = 5.5; // M3 nut across-flats
PIVOT_NUT_H = 2.4; // M3 nut height
DETENT_D = 3.2; // detent notch diameter (15° step notches on base ear)
DETENT_R = 8.0; // detent notch radius from pivot centre
// ── Anchor cradle geometry ───────────────────────────────────────────────────
CRADLE_WALL_T = 3.5; // side wall thickness
CRADLE_BACK_T = 4.0; // back wall thickness (label slot in here)
CRADLE_FLOOR_T = 3.0; // floor thickness
CRADLE_LIP_H = 4.0; // front retaining lip height
CRADLE_LIP_T = 2.5; // front lip thickness
STANDOFF_H = 3.0; // M2.5 standoff height (PCB clear of floor)
STANDOFF_OD = 5.5; // standoff boss OD
LABEL_W = UWB_L - 4.0; // label slot width
LABEL_H = UWB_W * 0.55; // label slot height (~half PCB width)
LABEL_T = 1.2; // label card thickness (paper + laminate)
// ── USB-C cable channel ──────────────────────────────────────────────────────
USBC_CHAN_W = 11.0; // channel width (USB-C plug body ~8.5 mm)
USBC_CHAN_H = 7.0; // channel height (plug + cable radius)
// ── Cable guide clip ─────────────────────────────────────────────────────────
CLIP_CABLE_D = 4.5; // USB-C cable OD
CLIP_T = 2.0; // clip wall thickness
CLIP_BODY_W = 16.0; // clip body width
CLIP_BODY_H = 10.0; // clip body height
// ── Fastener sizes ────────────────────────────────────────────────────────────
M2P5_D = 2.7; // M2.5 clearance
M3_D = 3.3;
M4_D = 4.3;
M3_NUT_AF = 5.5;
M3_NUT_H = 2.4;
// ============================================================
// RENDER DISPATCH
// ============================================================
RENDER = "assembly";
if (RENDER == "assembly") assembly_preview();
else if (RENDER == "wall_base_stl") wall_base();
else if (RENDER == "tilt_arm_stl") tilt_arm();
else if (RENDER == "anchor_cradle_stl") anchor_cradle();
else if (RENDER == "cable_clip_stl") cable_clip();
// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly_preview() {
// Ghost wall surface
%color("Wheat", 0.25)
translate([-BASE_W/2, -10, -BASE_H/2])
cube([BASE_W, 10, BASE_H + 40]);
// Wall base
color("OliveDrab", 0.85)
wall_base();
// Tilt arm at TILT_DEG
color("SteelBlue", 0.85)
translate([0, KNUCKLE_T, 0])
rotate([TILT_DEG, 0, 0])
tilt_arm();
// Anchor cradle at end of arm
color("DarkSlateGray", 0.85)
translate([0, KNUCKLE_T, 0])
rotate([TILT_DEG, 0, 0])
translate([0, ARM_T, ARM_L])
anchor_cradle();
// ESP32 UWB Pro PCB ghost
%color("ForestGreen", 0.4)
translate([0, KNUCKLE_T, 0])
rotate([TILT_DEG, 0, 0])
translate([-UWB_L/2,
ARM_T + CRADLE_BACK_T,
ARM_L + CRADLE_FLOOR_T + STANDOFF_H])
cube([UWB_L, UWB_W, UWB_H]);
// Cable clip on arm mid-point
color("DimGray", 0.70)
translate([ARM_W/2, KNUCKLE_T, 0])
rotate([TILT_DEG, 0, 0])
translate([0, ARM_T + e, ARM_L/2])
rotate([0, -90, 90])
cable_clip();
}
// ============================================================
// PART 1 — WALL BASE
// ============================================================
// Flat backplate screws to wall or ceiling with 2× countersunk
// M4 / #6 wood screws on BASE_SCREW_SPC (35 mm) centres.
// Two upstanding pivot ears straddle the tilt arm; M3 pivot bolt
// passes through both ears and arm knuckle.
// Detent arc on inner face of each ear: 7 notches at 15° steps
// (0°90°) so tilt angle can be set without a protractor.
// Label slot recess on outer face identifies anchor installation zone.
//
// Dual-use: mount flat face to wall (screws vertical) for wall mount,
// or flat face to ceiling (screws horizontal) for overhead mount.
//
// Print: backplate flat on bed, PETG, 5 perims, 40 % gyroid.
module wall_base() {
ear_h = ARM_W + 3.0; // ear height (spans arm width + clearance)
ear_t = 6.0; // ear thickness (Y)
ear_sep = ARM_W + 1.0; // gap between ear inner faces (arm clearance)
difference() {
union() {
// ── Backplate ────────────────────────────────────────────────
translate([-BASE_W/2, -BASE_T, -BASE_H/2])
cube([BASE_W, BASE_T, BASE_H]);
// ── Two pivot ears (straddle tilt arm) ───────────────────────
for (ex = [-(ear_sep/2 + ear_t), ear_sep/2])
translate([ex, -BASE_T + e, -ear_h/2])
cube([ear_t, KNUCKLE_T + e, ear_h]);
// ── Stiffening gussets between backplate and ears ────────────
for (ex = [-(ear_sep/2 + ear_t), ear_sep/2])
hull() {
translate([ex, -BASE_T, -ear_h/2])
cube([ear_t, BASE_T - 1, 2]);
translate([ex, -BASE_T, ear_h/2 - 2])
cube([ear_t, BASE_T - 1, 2]);
translate([ex + (ex < 0 ? ear_t : 0), -BASE_T, -ear_h/4])
cube([1, 1, ear_h/2]);
}
}
// ── 2× countersunk wall screws (centred X, BASE_SCREW_SPC Z span) ──
for (sz = [-BASE_SCREW_SPC/2, BASE_SCREW_SPC/2]) {
// Through bore
translate([0, -BASE_T - e, sz])
rotate([-90, 0, 0])
cylinder(d = BASE_SCREW_D, h = BASE_T + 2*e);
// Countersink (rear face of backplate)
translate([0, -BASE_T - e, sz])
rotate([-90, 0, 0])
cylinder(d1 = BASE_SCREW_HD, d2 = BASE_SCREW_D,
h = BASE_SCREW_HH + e);
}
// ── Pivot bolt bore (M3, through both ears) ──────────────────────
translate([-(ear_sep/2 + ear_t + e), KNUCKLE_T * 0.55, 0])
rotate([0, 90, 0])
cylinder(d = PIVOT_D, h = ear_sep + 2*ear_t + 2*e);
// ── M3 nyloc nut pocket (outer face of one ear) ──────────────────
translate([ear_sep/2 + ear_t - PIVOT_NUT_H - 0.4,
KNUCKLE_T * 0.55, 0])
rotate([0, 90, 0])
cylinder(d = PIVOT_NUT_AF / cos(30),
h = PIVOT_NUT_H + 0.5, $fn = 6);
// ── Detent arc (7 notches at 15° steps on inner ear face) ────────
// Notches on +X ear inner face (X side of ear at ear_sep/2)
for (da = [0 : 15 : 90])
translate([ear_sep/2 - e,
KNUCKLE_T * 0.55 + DETENT_R * sin(da),
DETENT_R * cos(da)])
rotate([0, 90, 0])
cylinder(d = DETENT_D, h = ear_t * 0.4 + e);
// ── Anchor zone label recess (rear of backplate, readable at install) ─
// Shallow pocket (1.5 mm deep) for a printed paper label strip
translate([0, -BASE_T - e, 0])
rotate([-90, 0, 0])
cube([BASE_W - 12, BASE_H - 16, 1.6], center = true);
// ── Lightening pockets ────────────────────────────────────────────
translate([0, -BASE_T + 1.5, 0])
cube([BASE_W - 14, BASE_T - 3, BASE_H - 20], center = true);
}
}
// ============================================================
// PART 2 — TILT ARM
// ============================================================
// Pivoting arm connecting the wall base to the anchor cradle.
// Knuckle end (Z=0 here) has M3 pivot bore and a detent ball spring
// plunger pocket that indexes into wall_base ear detent arc.
// Cradle end (+Z) has two M3 attachment bores for anchor_cradle.
// USB-C cable channel runs along outer face (+Y) of arm.
// Arm width = ARM_W; constrained to fit between base ears.
//
// Print: flat (knuckle face down), PETG, 5 perims, 40 % gyroid.
module tilt_arm() {
total_h = ARM_L + 10; // includes knuckle boss height
difference() {
union() {
// ── Arm body ─────────────────────────────────────────────────
translate([-ARM_W/2, 0, 0])
cube([ARM_W, ARM_T, total_h]);
// ── Knuckle boss (pivot end, Z=0) ────────────────────────────
translate([0, ARM_T/2, 0])
rotate([90, 0, 0])
cylinder(d = ARM_W, h = ARM_T, center = true);
// ── Cradle attach boss (Z = ARM_L) ───────────────────────────
translate([-ARM_W/2, 0, ARM_L])
cube([ARM_W, ARM_T + CRADLE_BACK_T, ARM_T]);
}
// ── M3 pivot bore (through knuckle, X axis) ───────────────────────
translate([-ARM_W/2 - e, ARM_T/2, 0])
rotate([0, 90, 0])
cylinder(d = PIVOT_D, h = ARM_W + 2*e);
// ── Detent plunger pocket (spring-ball indexing, +Y face) ─────────
// 3 mm dia × 4 mm deep pocket on knuckle outer face
translate([0, ARM_T + e, 0])
rotate([90, 0, 0])
cylinder(d = 3.2, h = 4 + e);
// ── USB-C cable channel (outer face +Y, runs full arm length) ─────
translate([-USBC_CHAN_W/2, ARM_T - e, ARM_T + 4])
cube([USBC_CHAN_W, USBC_CHAN_H, ARM_L - ARM_T - 4 - 4]);
// ── Cradle attach bolt holes (2× M3, at cradle end) ──────────────
for (bx = [-ARM_W/4, ARM_W/4])
translate([bx, ARM_T/2, ARM_L + ARM_T/2])
rotate([90, 0, 0])
cylinder(d = M3_D, h = ARM_T + CRADLE_BACK_T + 2*e);
// ── M3 nut pockets (rear of cradle attach boss) ───────────────────
for (bx = [-ARM_W/4, ARM_W/4])
translate([bx, ARM_T/2, ARM_L + ARM_T/2])
rotate([-90, 0, 0])
cylinder(d = M3_NUT_AF / cos(30),
h = M3_NUT_H + 0.5, $fn = 6);
// ── Lightening pocket in arm body ─────────────────────────────────
translate([0, ARM_T/2, ARM_L/2])
cube([ARM_W - 4, ARM_T - 2, ARM_L - 18], center = true);
}
}
// ============================================================
// PART 3 — ANCHOR CRADLE
// ============================================================
// Open-front U-cradle holding the ESP32 UWB Pro PCB.
// PCB retained on 4× M2.5 standoffs (UWB_HOLE_X × UWB_HOLE_Y pattern).
// Back wall has:
// • USB-C exit slot (centred on PCB short edge, near floor)
// • Label window slot (top half of back wall) — insert printed
// card strip to identify anchor ID (e.g. "UWB-A3 NE-CORNER")
// Front retaining lip prevents PCB from sliding forward.
// Antenna keep-out: top face is fully open; back wall material
// stays below UWB_ANTENNA_L from PCB rear so antenna is unobstructed.
//
// Cradle attaches to tilt_arm via 2× M3 bolts through back wall tabs.
//
// Print: back wall flat on bed, PETG, 5 perims, 40 % gyroid.
module anchor_cradle() {
outer_l = UWB_L + 2*CRADLE_WALL_T;
outer_w = UWB_W + CRADLE_FLOOR_T;
pcb_z = CRADLE_FLOOR_T + STANDOFF_H;
difference() {
union() {
// ── Cradle body ───────────────────────────────────────────────
translate([-outer_l/2, 0, 0])
cube([outer_l, outer_w, UWB_H + pcb_z + 2]);
// ── Front retaining lip ───────────────────────────────────────
translate([-outer_l/2, outer_w - CRADLE_LIP_T, 0])
cube([outer_l, CRADLE_LIP_T, CRADLE_LIP_H]);
// ── Arm attachment tabs (extend behind back wall) ─────────────
for (tx = [-ARM_W/4, ARM_W/4])
translate([tx - 4, -CRADLE_BACK_T, 0])
cube([8, CRADLE_BACK_T + 1, UWB_H + pcb_z + 2]);
}
// ── PCB pocket (hollow interior) ──────────────────────────────────
translate([-UWB_L/2, 0, pcb_z])
cube([UWB_L, UWB_W + 1, UWB_H + 4]);
// ── 4× M2.5 standoff bores (hole through cradle floor) ────────────
for (hx = [-UWB_HOLE_X/2, UWB_HOLE_X/2])
for (hy = [CRADLE_FLOOR_T/2, CRADLE_FLOOR_T/2 + UWB_HOLE_Y])
translate([hx, hy, -e])
cylinder(d = M2P5_D, h = pcb_z + 2*e);
// ── M2.5 standoff boss subtraction (leave boss, subtract floor) ──
// (bosses are the remaining solid cylinders after hollowing pocket)
// ── USB-C exit slot (back wall, aligned to PCB short edge) ────────
// PCB USB-C is on Y face (back wall side); slot through back wall
translate([0, -CRADLE_BACK_T - e, pcb_z + UWB_H/2 - UWB_USBC_H/2])
cube([UWB_USBC_W + 2, CRADLE_BACK_T + 2*e, UWB_USBC_H + 2],
center = [true, false, false]);
// ── USB-C cable routing groove (outer face of back wall) ──────────
translate([0, -CRADLE_BACK_T - e, -e])
cube([USBC_CHAN_W, USBC_CHAN_H, pcb_z + UWB_H/2 + USBC_CHAN_H],
center = [true, false, false]);
// ── Label card slot (back wall exterior, top half) ────────────────
// Insert paper/card label strip to identify this anchor instance
translate([0, -CRADLE_BACK_T - e, pcb_z + UWB_H/2])
cube([LABEL_W, LABEL_T + 0.3, LABEL_H],
center = [true, false, false]);
// ── Antenna keep-out cutout in back wall top section ──────────────
// Remove material from back wall above antenna line so PETG does
// not block UWB signal from the rear half of PCB
translate([0, -e, pcb_z + UWB_H - UWB_ANTENNA_L])
cube([UWB_L - 4, CRADLE_BACK_T + 2*e, UWB_ANTENNA_L + 4],
center = [true, false, false]);
// ── Arm attachment bolt holes (through back wall tabs) ────────────
for (tx = [-ARM_W/4, ARM_W/4])
translate([tx, ARM_T/2 - CRADLE_BACK_T, UWB_H/2 + pcb_z/2])
rotate([-90, 0, 0])
cylinder(d = M3_D, h = ARM_T + CRADLE_BACK_T + 2*e);
// ── Lightening slots in side walls ────────────────────────────────
for (side = [-outer_l/2 - e, outer_l/2 - CRADLE_WALL_T - e])
translate([side, 2, pcb_z + 2])
cube([CRADLE_WALL_T + 2*e, UWB_W - 4, UWB_H - 4]);
}
// ── M2.5 standoff posts (positive geometry, inside cradle) ────────────
for (hx = [-UWB_HOLE_X/2, UWB_HOLE_X/2])
for (hy = [CRADLE_FLOOR_T/2, CRADLE_FLOOR_T/2 + UWB_HOLE_Y])
difference() {
translate([hx, hy, CRADLE_FLOOR_T - e])
cylinder(d = STANDOFF_OD, h = STANDOFF_H + e);
translate([hx, hy, CRADLE_FLOOR_T - 2*e])
cylinder(d = M2P5_D, h = STANDOFF_H + 4);
}
}
// ============================================================
// PART 4 — CABLE CLIP
// ============================================================
// Snap-on C-clip retaining USB-C cable along tilt_arm outer face.
// Presses onto arm edge (ARM_T width) with flexible PETG snap tongues.
// Cable sits in semicircular channel; open front for push-in install.
// Print ×23 per anchor (space 25 mm apart along arm).
//
// Print: clip-opening face down, PETG, 3 perims, 20 % infill.
module cable_clip() {
ch_r = CLIP_CABLE_D/2 + CLIP_T; // channel outer radius
snap_t = 1.6; // snap tongue thickness
difference() {
union() {
// ── Body plate (sits on arm face) ─────────────────────────────
translate([-CLIP_BODY_W/2, 0, 0])
cube([CLIP_BODY_W, CLIP_T, CLIP_BODY_H]);
// ── Cable channel (C-shape, opening toward +Y) ────────────────
translate([0, CLIP_T + ch_r, CLIP_BODY_H/2])
rotate([0, 90, 0])
difference() {
cylinder(r = ch_r, h = CLIP_BODY_W, center = true);
cylinder(r = CLIP_CABLE_D/2, h = CLIP_BODY_W + 2*e,
center = true);
translate([0, ch_r + e, 0])
cube([CLIP_CABLE_D * 0.85,
ch_r * 2 + 2*e,
CLIP_BODY_W + 2*e], center = true);
}
// ── Snap tongues (straddle arm edges, -Y side of body plate) ──
for (tx = [-CLIP_BODY_W/2 + 1.5, CLIP_BODY_W/2 - 1.5 - snap_t])
translate([tx, -ARM_T - 1, 0])
cube([snap_t, ARM_T + 1 + CLIP_T, CLIP_BODY_H]);
// ── Snap barbs (grip underside of arm) ────────────────────────
for (tx = [-CLIP_BODY_W/2 + 1.5, CLIP_BODY_W/2 - 1.5 - snap_t])
translate([tx + snap_t/2, -ARM_T - 1, CLIP_BODY_H/2])
rotate([0, 90, 0])
cylinder(d = 2, h = snap_t, center = true);
}
// ── Arm slot (arm body passes between tongues) ─────────────────────
translate([0, -ARM_T - 1 - e, CLIP_BODY_H/2])
cube([CLIP_BODY_W - 6, ARM_T + 2, CLIP_BODY_H - 4], center = true);
}
}