saltylab-firmware/chassis/uwb_anchor_mount.scad

464 lines
22 KiB
OpenSCAD
Raw Permalink 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; 15° detent steps
// locked with M3 nyloc bolt; range 090°
// Anchor cradle→ U-cradle holding ESP32 UWB Pro PCB on M2.5 standoffs
// USB-C channel→ routed groove on tilt arm + exit slot in cradle back wall
// Label slot → rear window slot for printed anchor-ID card strip
//
// Part catalogue:
// Part 1 — wall_base() Backplate + 2-ear pivot block + detent arc
// Part 2 — tilt_arm() Pivoting arm with knuckle + cradle stub
// Part 3 — anchor_cradle() PCB cradle, 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, UWB_USBC_W × UWB_USBC_H
// Antenna area : top face rear half — 10 mm keep-out of bracket material
//
// Tilt angles (15° detent steps, set TILT_DEG before export):
// 0° → horizontal face-up (ceiling, antenna faces down)
// 30° → 30° downward (wall near ceiling) [default]
// 45° → diagonal (wall mid-height)
// 90° → vertical face-out (wall, 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;
// ── ESP32 UWB Pro PCB dimensions (verify with calipers) ──────────────────────
UWB_L = 55.0; // PCB length
UWB_W = 28.0; // PCB width
UWB_H = 10.0; // PCB + components height
UWB_HOLE_X = 47.5; // M2.5 hole X span
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 at PCB rear (keep-out)
// ── Wall base geometry ────────────────────────────────────────────────────────
BASE_W = 60.0;
BASE_H = 50.0;
BASE_T = 5.0;
BASE_SCREW_D = 4.5; // M4 clearance
BASE_SCREW_HD = 8.5; // countersink OD
BASE_SCREW_HH = 3.5; // countersink depth
BASE_SCREW_SPC = 35.0; // Z span between screw holes
KNUCKLE_T = BASE_T + 4.0; // pivot ear depth (Y)
// ── Tilt arm geometry ─────────────────────────────────────────────────────────
ARM_W = 12.0;
ARM_T = 5.0;
ARM_L = 35.0;
PIVOT_D = 3.3; // M3 clearance
PIVOT_NUT_AF = 5.5;
PIVOT_NUT_H = 2.4;
DETENT_D = 3.2; // detent notch diameter
DETENT_R = 8.0; // detent notch radius from pivot
// ── Anchor cradle geometry ────────────────────────────────────────────────────
CRADLE_WALL_T = 3.5;
CRADLE_BACK_T = 4.0;
CRADLE_FLOOR_T = 3.0;
CRADLE_LIP_H = 4.0;
CRADLE_LIP_T = 2.5;
STANDOFF_H = 3.0;
STANDOFF_OD = 5.5;
LABEL_W = UWB_L - 4.0;
LABEL_H = UWB_W * 0.55;
LABEL_T = 1.2; // label card thickness
// ── USB-C cable routing ───────────────────────────────────────────────────────
USBC_CHAN_W = 11.0;
USBC_CHAN_H = 7.0;
// ── Cable clip ────────────────────────────────────────────────────────────────
CLIP_CABLE_D = 4.5;
CLIP_T = 2.0;
CLIP_BODY_W = 16.0;
CLIP_BODY_H = 10.0;
// ── Fasteners ─────────────────────────────────────────────────────────────────
M2P5_D = 2.7;
M3_D = 3.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.22)
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, pivoting at knuckle
color("SteelBlue", 0.85)
translate([0, KNUCKLE_T, 0])
rotate([TILT_DEG, 0, 0])
tilt_arm();
// Anchor cradle at arm end
color("DarkSlateGray", 0.85)
translate([0, KNUCKLE_T, 0])
rotate([TILT_DEG, 0, 0])
translate([0, ARM_T, ARM_L])
anchor_cradle();
// PCB ghost
%color("ForestGreen", 0.38)
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 at 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 pivot ears straddle the tilt arm; M3 pivot bolt passes through
// both ears and arm knuckle.
// Detent arc on inner face of +X ear: 7 notches at 15° steps (090°)
// so tilt can be set without a protractor.
// Shallow rear recess accepts a printed installation-zone label.
//
// Dual-use: flat face to wall (vertical screw axis) or flat face
// to ceiling (horizontal screw axis) — same part either way.
//
// Print: backplate flat on bed, PETG, 5 perims, 40 % gyroid.
module wall_base() {
ear_h = ARM_W + 3.0;
ear_t = 6.0;
ear_sep = ARM_W + 1.0;
difference() {
union() {
// ── Backplate ────────────────────────────────────────────────
translate([-BASE_W/2, -BASE_T, -BASE_H/2])
cube([BASE_W, BASE_T, BASE_H]);
// ── Two pivot ears ────────────────────────────────────────────
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 ────────────────────────────────────────
for (ex = [-(ear_sep/2 + ear_t), ear_sep/2])
hull() {
translate([ex, -BASE_T, -ear_h/4])
cube([ear_t, BASE_T - 1, ear_h/2]);
translate([ex + (ex < 0 ? ear_t*0.6 : 0),
-BASE_T, -ear_h/6])
cube([ear_t * 0.4, 1, ear_h/3]);
}
}
// ── 2× countersunk wall screws ────────────────────────────────────
for (sz = [-BASE_SCREW_SPC/2, BASE_SCREW_SPC/2]) {
translate([0, -BASE_T - e, sz])
rotate([-90, 0, 0])
cylinder(d = BASE_SCREW_D, h = BASE_T + 2*e);
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 +X ear inner face ─────
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.45 + e);
// ── Installation label recess (rear face of backplate) ────────────
translate([0, -BASE_T - e, 0])
rotate([-90, 0, 0])
cube([BASE_W - 12, BASE_H - 16, 1.6], center = true);
// ── Lightening pocket ─────────────────────────────────────────────
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 linking wall_base pivot ears to anchor_cradle.
// Knuckle end (Z=0): M3 pivot bore + spring-plunger detent pocket
// that indexes into the base ear detent arc notches.
// Cradle end (Z=ARM_L): 2× M3 bolt attachment to cradle back wall.
// USB-C cable channel runs along outer (+Y) face, full arm length.
//
// Print: knuckle face flat on bed, PETG, 5 perims, 40 % gyroid.
module tilt_arm() {
total_h = ARM_L + 10;
difference() {
union() {
// ── Arm body ─────────────────────────────────────────────────
translate([-ARM_W/2, 0, 0])
cube([ARM_W, ARM_T, total_h]);
// ── Knuckle boss (rounded pivot end) ─────────────────────────
translate([0, ARM_T/2, 0])
rotate([90, 0, 0])
cylinder(d = ARM_W, h = ARM_T, center = true);
// ── Cradle attach stub (Z = ARM_L) ────────────────────────────
translate([-ARM_W/2, 0, ARM_L])
cube([ARM_W, ARM_T + CRADLE_BACK_T, ARM_T]);
}
// ── M3 pivot bore ─────────────────────────────────────────────────
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 (3 mm spring-ball, outer +Y face) ──────
translate([0, ARM_T + e, 0])
rotate([90, 0, 0])
cylinder(d = 3.2, h = 4 + e);
// ── USB-C cable channel (outer +Y face, mid-arm length) ───────────
translate([-USBC_CHAN_W/2, ARM_T - e, ARM_T + 4])
cube([USBC_CHAN_W, USBC_CHAN_H, ARM_L - ARM_T - 8]);
// ── Cradle attach bolt holes (2× M3 at cradle stub) ───────────────
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 (front of cradle stub) ─────────────────────────
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 ─────────────────────────────────────────────
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 for ESP32 UWB Pro PCB.
// PCB retained on 4× M2.5 standoffs matching UWB_HOLE_X × UWB_HOLE_Y.
// Back wall features:
// • USB-C exit slot — aligns with PCB USB-C port
// • USB-C groove — cable routes from slot toward arm channel
// • Label card slot — insert printed strip for anchor ID
// • Antenna keep-out — back wall material removed above antenna area
// Front lip prevents PCB from sliding forward.
// Two attachment tabs bolt to tilt_arm cradle stub.
//
// 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;
total_z = pcb_z + UWB_H + 2;
difference() {
union() {
// ── Cradle body ───────────────────────────────────────────────
translate([-outer_l/2, 0, 0])
cube([outer_l, outer_w, total_z]);
// ── 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 (behind back wall) ─────────────────────
for (tx = [-ARM_W/4, ARM_W/4])
translate([tx - 4, -CRADLE_BACK_T, 0])
cube([8, CRADLE_BACK_T + 1, total_z]);
}
// ── PCB pocket ────────────────────────────────────────────────────
translate([-UWB_L/2, 0, pcb_z])
cube([UWB_L, UWB_W + 1, UWB_H + 4]);
// ── USB-C exit slot (through back wall, aligned to PCB port) ─────
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 back wall face) ─────────────
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 (insert from below, rear face upper half) ─────
// Paper/laminate card strip identifying 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: remove back wall above antenna area ─────────
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 bolt holes through attachment tabs ────────────────────────
for (tx = [-ARM_W/4, ARM_W/4])
translate([tx, ARM_T/2 - CRADLE_BACK_T, total_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_x = [-outer_l/2 - e, outer_l/2 - CRADLE_WALL_T - e])
translate([side_x, 2, pcb_z + 2])
cube([CRADLE_WALL_T + 2*e, UWB_W - 4, UWB_H - 4]);
}
// ── M2.5 standoff bosses (positive, inside cradle floor) ──────────────
for (hx = [-UWB_HOLE_X/2, UWB_HOLE_X/2])
for (hy = [(outer_w - UWB_W)/2 + (UWB_W - UWB_HOLE_Y)/2,
(outer_w - UWB_W)/2 + (UWB_W - UWB_HOLE_Y)/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_T-wide arm with PETG snap tongues.
// Open-front cable channel for push-in cable insertion.
// Print ×23 per anchor, spaced 25 mm along arm.
//
// Print: clip-opening face down, PETG, 3 perims, 20 % infill.
module cable_clip() {
ch_r = CLIP_CABLE_D/2 + CLIP_T;
snap_t = 1.6;
difference() {
union() {
// ── Body plate ────────────────────────────────────────────────
translate([-CLIP_BODY_W/2, 0, 0])
cube([CLIP_BODY_W, CLIP_T, CLIP_BODY_H]);
// ── Cable channel (C-shape, opens 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);
// open insertion slot
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, -Y side of body) ─────────────
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 ────────────────────────────────────────────────
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);
}
}