1026 lines
50 KiB
OpenSCAD
1026 lines
50 KiB
OpenSCAD
// ============================================
|
|
// Tapo Ceiling Mount - Shared Parameters
|
|
// ============================================
|
|
// Common parameters, derived values and helper modules for the false-ceiling
|
|
// mount adapter for a TP-Link Tapo C202 / C210 camera.
|
|
//
|
|
// The mount clamps a suspended-ceiling panel between one exterior plate below
|
|
// the ceiling and two interior clamp pieces above it. Four M4 screws travel
|
|
// upward through the EXISTING ceiling hole only (no new holes are drilled) and
|
|
// engage captive M4 hex nuts in the two interior clamps.
|
|
//
|
|
// Load path:
|
|
// camera -> original Tapo base -> exterior plate -> M4 screws ->
|
|
// captive M4 nuts -> interior clamps -> upper surface of the ceiling panel.
|
|
//
|
|
// Coordinate system (assembly):
|
|
// Origin (0,0,0) = center of the ceiling hole, on the BOTTOM face of the
|
|
// exterior plate.
|
|
// Z = up (toward the space above the ceiling).
|
|
// X = left / right axis. The two interior clamps sit on +X and -X.
|
|
// Y = the open cable corridor. The clamps leave +Y and -Y open for cables.
|
|
//
|
|
// This file renders no geometry. Every part file includes it.
|
|
|
|
// ============================================
|
|
// CUSTOMIZABLE PARAMETERS
|
|
// ============================================
|
|
|
|
/* [Existing Ceiling Hole] */
|
|
// Diameter of the existing circular ceiling hole (mm). Measure the real hole.
|
|
ceiling_hole_diameter = 80; // [70:1:110]
|
|
// Thickness of the false-ceiling panel (mm). Measure the real panel.
|
|
ceiling_thickness = 12.5; // [6:0.5:30]
|
|
// Radial gap so printed parts do not scrape the hole edge (mm).
|
|
install_clearance = 1.5; // [0.5:0.1:4]
|
|
|
|
/* [Exterior Plate] */
|
|
// Outer diameter of the visible plate below the ceiling (mm).
|
|
outer_diameter = 105; // [90:1:180]
|
|
// Plate thickness (mm).
|
|
plate_thickness = 3; // [4:0.5:10]
|
|
// Chamfer on the visible outer edge of the plate (mm).
|
|
plate_edge_chamfer = 1.5; // [0:0.1:4]
|
|
// Height of the centering lip that enters the ceiling hole (mm).
|
|
centering_lip_height = 3; // [0:0.5:8]
|
|
// Radial gap between the lip and the hole edge (mm). Keep the lip loose.
|
|
centering_lip_clearance = 0.6;// [0:0.1:3]
|
|
// Radial wall thickness of the centering lip (mm).
|
|
centering_lip_wall = 3; // [1.5:0.5:6]
|
|
// Lead-in taper on the lip top edge for easy entry (mm).
|
|
centering_lip_lead = 1; // [0:0.1:3]
|
|
|
|
/* [Central Cable Opening] */
|
|
// Maximum width of the central cable opening (the pill) through the plate
|
|
// (mm). The model narrows it when needed, so it always passes between the
|
|
// screw bosses.
|
|
central_opening_diameter = 54;// [40:1:70]
|
|
// Direction of the cable-routing extension, measured from +X (degrees).
|
|
cable_slot_angle = 90; // [0:5:180]
|
|
// Extra reach of the opening on each side along the slot direction (mm).
|
|
cable_slot_reach = 8; // [0:0.5:20]
|
|
|
|
/* [M4 Fasteners] */
|
|
// One screw per clamp, on the clamp axis. The screw radius is derived: each
|
|
// screw and its top boss stay inside the centering lip that enters the hole.
|
|
// M4 screw clearance hole diameter (mm).
|
|
m4_clearance_d = 4.5; // [4.2:0.1:5.2]
|
|
// M4 hex nut across-flats dimension (mm). Standard M4 = 7.0.
|
|
m4_nut_flats = 7.0; // [6:0.1:9]
|
|
// M4 hex nut thickness (mm). Standard M4 = 3.2.
|
|
m4_nut_thickness = 3.2; // [2.5:0.1:5]
|
|
// Nut pocket across-flats offset from m4_nut_flats (mm). A negative value
|
|
// makes the pocket smaller than the nut, so the nut needs a firm push and
|
|
// stays in. Raise it when the nut does not go in, lower it when the nut falls
|
|
// out. Pick the value with tapo_ceiling_nut_fit_test.scad.
|
|
nut_pocket_tol = -0.15; // [-0.4:0.05:0.4]
|
|
// Extra nut pocket depth over m4_nut_thickness (mm). The nut seats fully
|
|
// below the boss top.
|
|
nut_pocket_depth_tol = 0.2; // [0:0.05:0.6]
|
|
// 45-degree lead-in chamfer at the nut pocket mouth, per side (mm). It starts
|
|
// the nut square before the press.
|
|
nut_pocket_lead = 0.4; // [0:0.1:1]
|
|
// General printed fit tolerance (mm). Reference value.
|
|
general_fit_tol = 0.25; // [0.1:0.05:0.5]
|
|
// Screw head style. cap = counterbore, countersunk = cone.
|
|
head_type = "cap"; // [cap, countersunk]
|
|
// Counterbore diameter for the screw head recess (mm).
|
|
screw_head_recess_d = 8.5; // [7:0.1:14]
|
|
// Counterbore depth for the screw head recess (mm).
|
|
head_recess_depth = 3.2; // [1.5:0.1:6]
|
|
// Countersink top diameter, used when head_type = countersunk (mm).
|
|
csk_d = 8.4; // [7:0.1:12]
|
|
// Countersink depth, used when head_type = countersunk (mm).
|
|
csk_depth = 2.4; // [1.5:0.1:5]
|
|
// Extra plate thickness added as a top boss around each screw (mm).
|
|
screw_boss_h = 3; // [0:0.5:8]
|
|
// Diameter of the plate top boss around each screw (mm).
|
|
screw_boss_d = 12; // [9:0.5:20]
|
|
|
|
/* [Interior Clamps] */
|
|
// Inner radius of each curved clamp (mm). Keeps the hole center open.
|
|
clamp_inner_r = 20; // [10:0.5:35]
|
|
// Outer radius of each curved clamp (mm). Must exceed the hole radius.
|
|
clamp_outer_r = 54; // [30:0.5:70]
|
|
// Half-angle of each clamp arc from its axis (degrees). Controls insertion.
|
|
clamp_half_angle = 40; // [20:1:70]
|
|
// Clamp thickness above the bearing face (mm). The top is flat, so the clamp
|
|
// prints top-down and its lip prints without support.
|
|
clamp_boss_height = 9; // [6:0.5:14]
|
|
// Wall thickness around the hex nut pocket (mm).
|
|
clamp_boss_wall = 2.5; // [1.5:0.5:5]
|
|
// Plan corner fillet radius of the clamp arc (mm).
|
|
clamp_corner_r = 4; // [0:0.5:10]
|
|
// Add a centering lip under each clamp. It drops into the hole from above,
|
|
// like the plate lip from below, so the clamp seats itself and cannot turn
|
|
// around its screw.
|
|
clamp_lip = true; // [true, false]
|
|
// Height of the clamp lip below the bearing face (mm).
|
|
clamp_lip_h = 3; // [0.5:0.5:6]
|
|
// Join the two clamps with a thin flexible line, so they print and go in as
|
|
// one part.
|
|
clamp_link = true; // [true, false]
|
|
// Width and thickness of the clamp link line (mm).
|
|
clamp_link_w = 2; // [1:0.5:4]
|
|
clamp_link_t = 1.2; // [0.6:0.2:2]
|
|
// Cut anti-slip grooves into the bearing face.
|
|
clamp_grip_ribs = true; // [true, false]
|
|
// Depth of the anti-slip grooves (mm).
|
|
clamp_groove_depth = 0.5; // [0:0.1:2]
|
|
// Radial pitch of the anti-slip grooves (mm).
|
|
clamp_groove_pitch = 3; // [1.5:0.5:6]
|
|
// Width of each anti-slip groove (mm).
|
|
clamp_groove_w = 1; // [0.4:0.1:3]
|
|
// Cut a shallow recess for an optional TPU / rubber pad.
|
|
clamp_tpu_pad = false; // [true, false]
|
|
// Depth of the TPU pad recess (mm).
|
|
clamp_pad_depth = 1.5; // [0:0.1:4]
|
|
// Inset of the TPU pad recess from the clamp edge (mm).
|
|
clamp_pad_inset = 3; // [1:0.5:8]
|
|
|
|
/* [Camera Base Mount] */
|
|
// Distance between the two Tapo base mounting holes (mm). The original Tapo
|
|
// base uses 39 mm.
|
|
camera_mount_hole_spacing = 39; // [10:0.5:70]
|
|
// Diameter of each Tapo base mounting hole (mm). 2.9 self-taps the ST3.5
|
|
// screws that come with the Tapo base.
|
|
camera_mount_hole_diameter = 2.9; // [2:0.1:5]
|
|
// Rotation of the base mounting pattern around the plate center (degrees).
|
|
camera_mount_rotation = 0; // [0:5:180]
|
|
// Offset of the base mounting pattern along X (mm).
|
|
camera_mount_offset_x = 0; // [-20:0.5:20]
|
|
// Offset of the base mounting pattern along Y (mm).
|
|
camera_mount_offset_y = 0; // [-20:0.5:20]
|
|
// Diameter of the solid boss under each base mounting hole (mm).
|
|
camera_boss_d = 10; // [6:0.5:16]
|
|
// Depth of each base mounting pilot hole (mm).
|
|
camera_screw_depth = 5; // [2:0.5:8]
|
|
// Width of the rib that ties each base boss to the plate ring (mm).
|
|
camera_rib_w = 4; // [2:0.5:8]
|
|
// Reference footprint diameter of the original Tapo base (mm).
|
|
camera_base_diameter = 60; // [30:1:90]
|
|
|
|
/* [Quality] */
|
|
// Facet count for smooth printable circles.
|
|
$fn = 96; // [48:8:200]
|
|
|
|
// ============================================
|
|
// DERIVED VALUES
|
|
// ============================================
|
|
|
|
hole_r = ceiling_hole_diameter / 2;
|
|
plate_r = outer_diameter / 2;
|
|
lip_outer_r = hole_r - centering_lip_clearance;
|
|
lip_inner_r = lip_outer_r - centering_lip_wall;
|
|
// Screw axis radius. The top boss around the screw stays inside the lip
|
|
// outline at every height, including the lead-in chamfer at the lip top, so
|
|
// nothing that enters the ceiling hole sticks out past the lip.
|
|
lip_top_outer_r = lip_outer_r - centering_lip_lead;
|
|
screw_r = lip_top_outer_r - screw_boss_d / 2 - 0.3;
|
|
// Pill half-width. It shrinks when needed to keep 1 mm clear of the bosses.
|
|
central_opening_r = min(central_opening_diameter / 2,
|
|
screw_r - screw_boss_d / 2 - 1
|
|
- cable_slot_reach * abs(cos(cable_slot_angle)));
|
|
|
|
// Nut pocket across flats (press fit) and depth (one nut plus an allowance).
|
|
nut_pocket_flats = m4_nut_flats + nut_pocket_tol;
|
|
nut_pocket_depth = m4_nut_thickness + nut_pocket_depth_tol;
|
|
// Boss diameter houses the hex nut across-corners plus two walls.
|
|
clamp_boss_d = m4_nut_flats / cos(30) + 2 * clamp_boss_wall;
|
|
|
|
// The two screw positions: one on the +X clamp axis, one on the -X clamp axis.
|
|
clamp_center_angles = [0, 180];
|
|
screw_positions = [ for (c = clamp_center_angles) [ screw_r * cos(c), screw_r * sin(c) ] ];
|
|
|
|
// Screw position for a single clamp built centered on the +X axis.
|
|
clamp_local_screws = [ [ screw_r, 0 ] ];
|
|
|
|
// Factory Tapo base screw positions on the original-base adapter. The base
|
|
// has its screws at 90 degrees to its wings, so they follow tapo_wing_angle.
|
|
function tapo_base_screw_xy() = [ for (s = [-1, 1])
|
|
rot2([ s * camera_mount_hole_spacing / 2, 0 ], cable_exit_angle + tapo_wing_angle + 90) ];
|
|
|
|
// Camera base mounting positions after spacing, rotation and offset.
|
|
function rot2(v, a) = [ v[0] * cos(a) - v[1] * sin(a),
|
|
v[0] * sin(a) + v[1] * cos(a) ];
|
|
camera_mount_xy = [ for (s = [-1, 1])
|
|
rot2([ s * camera_mount_hole_spacing / 2, 0 ], camera_mount_rotation)
|
|
+ [ camera_mount_offset_x, camera_mount_offset_y ] ];
|
|
|
|
// Recommended M4 screw length (under-head), rounded up to the next 5mm.
|
|
nut_top_z_assembly = plate_thickness + ceiling_thickness + clamp_boss_height;
|
|
under_head_needed = nut_top_z_assembly - head_recess_depth;
|
|
recommended_screw_length = ceil(under_head_needed / 5) * 5;
|
|
|
|
// Largest chord of a clamp footprint. It must pass through the ceiling hole.
|
|
clamp_chord = 2 * clamp_outer_r * sin(clamp_half_angle);
|
|
|
|
// ============================================
|
|
// PARAMETER VALIDATION
|
|
// ============================================
|
|
|
|
assert(outer_diameter > ceiling_hole_diameter,
|
|
"Outer plate must be larger than the ceiling hole.");
|
|
assert(screw_r + screw_boss_d / 2 < lip_top_outer_r,
|
|
"A screw boss reaches past the centering lip. Reduce screw_boss_d.");
|
|
assert(clamp_outer_r > hole_r,
|
|
"Clamp outer radius must exceed the hole radius to bear on the ceiling.");
|
|
assert(clamp_chord < ceiling_hole_diameter - 2 * install_clearance,
|
|
"Clamp is too wide to insert through the hole. Reduce clamp_half_angle or clamp_outer_r.");
|
|
assert(central_opening_r + cable_slot_reach < lip_inner_r,
|
|
"Central cable opening reaches the centering lip. Reduce it.");
|
|
assert(central_opening_r >= 20,
|
|
"Central cable opening is narrower than 40 mm. Reduce screw_boss_d or cable_slot_reach.");
|
|
assert(2 * screw_r > camera_base_diameter,
|
|
"Screws must clear the camera base so they stay accessible.");
|
|
|
|
echo(str("Recommended M4 screw length (under head): ", recommended_screw_length, " mm"));
|
|
echo(str("Clamp insertion chord: ", clamp_chord, " mm (hole ", ceiling_hole_diameter, " mm)"));
|
|
echo(str("Ceiling clamp overlap per side: ", clamp_outer_r - hole_r, " mm"));
|
|
echo(str("Screw axis radius: ", screw_r, " mm; boss edge ", screw_r + screw_boss_d / 2,
|
|
" mm; lip top edge ", lip_top_outer_r, " mm; hole radius ", hole_r, " mm"));
|
|
echo(str("Cable pill: ", 2 * central_opening_r, " x ",
|
|
2 * (central_opening_r + cable_slot_reach), " mm"));
|
|
|
|
nut_pocket_wall = clamp_boss_d / 2 - nut_pocket_flats / (2 * cos(30));
|
|
nut_pocket_floor = clamp_boss_height - nut_pocket_depth;
|
|
assert(nut_pocket_depth >= m4_nut_thickness,
|
|
"Nut pocket is shallower than the nut. Raise nut_pocket_depth_tol.");
|
|
assert(nut_pocket_wall >= 1.5,
|
|
"Wall around the nut pocket is under 1.5 mm. Raise clamp_boss_wall or lower nut_pocket_tol.");
|
|
assert(!clamp_lip || clamp_lip_h + max(centering_lip_height, screw_boss_h) <= ceiling_thickness - 1,
|
|
"The clamp lip meets the plate lip inside the hole. Lower clamp_lip_h.");
|
|
assert(nut_pocket_floor >= 2,
|
|
"Floor under the nut pocket is under 2 mm. Raise clamp_boss_height.");
|
|
echo(str("Nut pocket: ", nut_pocket_flats, " mm across flats (nut ", m4_nut_flats,
|
|
"), ", nut_pocket_depth, " mm deep, ", nut_pocket_flats + 2 * nut_pocket_lead,
|
|
" mm at the mouth; wall ", nut_pocket_wall, " mm; floor ", nut_pocket_floor, " mm"));
|
|
|
|
// ============================================
|
|
// SHARED HELPER MODULES
|
|
// ============================================
|
|
|
|
// Round the convex corners of a 2D shape to radius r.
|
|
module round2d(r) {
|
|
if (r > 0) offset(r = r) offset(delta = -r) children();
|
|
else children();
|
|
}
|
|
|
|
// A hexagonal prism with the given across-flats dimension and height.
|
|
module hex_prism(across_flats, h) {
|
|
rotate([0, 0, 30])
|
|
cylinder(h = h, r = across_flats / (2 * cos(30)), $fn = 6);
|
|
}
|
|
|
|
// 2D plan of one clamp arc, centered on the +X axis, with rounded corners.
|
|
module clamp_wedge_2d() {
|
|
R = clamp_outer_r + 2;
|
|
fan = [ for (t = [-clamp_half_angle : 2 : clamp_half_angle])
|
|
[ R * cos(t), R * sin(t) ] ];
|
|
round2d(clamp_corner_r)
|
|
intersection() {
|
|
difference() {
|
|
circle(r = clamp_outer_r);
|
|
circle(r = clamp_inner_r);
|
|
}
|
|
polygon(concat([[0, 0]], fan));
|
|
}
|
|
}
|
|
|
|
// Centering lip under the bearing face, just inside the hole edge. It mirrors
|
|
// the plate lip: same radii and lead-in chamfer, pointing down into the hole.
|
|
module clamp_lip() {
|
|
pts = [ [lip_inner_r, 0.01], [lip_outer_r, 0.01],
|
|
[lip_outer_r - centering_lip_lead, -clamp_lip_h], [lip_inner_r, -clamp_lip_h] ];
|
|
profile_arc(pts, 0, 2 * (clamp_half_angle - 4));
|
|
}
|
|
|
|
// The cut for one screw and one captive nut, with the pocket mouth at top_z.
|
|
// The nut pocket opens UPWARD and sits on a solid floor, so the nut cannot
|
|
// fall out during overhead work. The screw enters from the bearing face.
|
|
// The straight pocket walls grip the nut flats as a press fit. A 45-degree
|
|
// lead-in at the mouth centers the nut before the press.
|
|
module clamp_nut_cut(flats = nut_pocket_flats, top_z = clamp_boss_height) {
|
|
translate([0, 0, -1])
|
|
cylinder(d = m4_clearance_d, h = top_z + 2);
|
|
translate([0, 0, top_z - nut_pocket_depth])
|
|
hex_prism(flats, nut_pocket_depth + 1);
|
|
// The chamfer runs 1 mm past the mouth so the cut leaves no skin.
|
|
if (nut_pocket_lead > 0)
|
|
translate([0, 0, top_z - nut_pocket_lead])
|
|
rotate([0, 0, 30])
|
|
cylinder(h = nut_pocket_lead + 1,
|
|
r1 = flats / (2 * cos(30)),
|
|
r2 = (flats + 2 * (nut_pocket_lead + 1)) / (2 * cos(30)),
|
|
$fn = 6);
|
|
}
|
|
|
|
// Anti-slip grooves cut into the bearing face, limited to the clamp footprint.
|
|
module clamp_grooves() {
|
|
intersection() {
|
|
translate([0, 0, -0.01])
|
|
linear_extrude(clamp_groove_depth) clamp_wedge_2d();
|
|
union() {
|
|
for (gr = [hole_r : clamp_groove_pitch : clamp_outer_r])
|
|
difference() {
|
|
cylinder(r = gr + clamp_groove_w / 2, h = clamp_groove_depth);
|
|
translate([0, 0, -1])
|
|
cylinder(r = gr - clamp_groove_w / 2,
|
|
h = clamp_groove_depth + 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Optional shallow pocket for a TPU / rubber grip pad on the bearing face.
|
|
// It covers only the part that bears on the ceiling, outside the hole.
|
|
module clamp_pad_cut() {
|
|
translate([0, 0, -0.01])
|
|
linear_extrude(clamp_pad_depth)
|
|
difference() {
|
|
offset(r = -clamp_pad_inset) clamp_wedge_2d();
|
|
circle(r = hole_r + clamp_pad_inset);
|
|
}
|
|
}
|
|
|
|
// One complete interior clamp, centered on the +X axis. The bearing face is
|
|
// the z = 0 plane, the lip hangs below it and the nut pocket opens at the
|
|
// flat top. Print it top-down: the top on the bed, the lip pointing up.
|
|
module inner_clamp_body() {
|
|
difference() {
|
|
union() {
|
|
// One flat-topped slab. It prints top-down, so the lip under it
|
|
// prints upward without support.
|
|
linear_extrude(clamp_boss_height) clamp_wedge_2d();
|
|
if (clamp_lip) clamp_lip();
|
|
}
|
|
for (p = clamp_local_screws)
|
|
translate([p[0], p[1], 0]) clamp_nut_cut();
|
|
if (clamp_grip_ribs) clamp_grooves();
|
|
if (clamp_tpu_pad) clamp_pad_cut();
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// MODULAR CAMERA CONNECTOR - PARAMETERS
|
|
// ============================================
|
|
// A modular camera interface hangs below the exterior plate. It adds no load
|
|
// to the ceiling clamp system. Architecture:
|
|
// exterior plate -> connector collar -> camera adapter -> camera.
|
|
// Two bayonet interfaces join the layers. Both use the same mechanism: the
|
|
// fixed part carries a short ring with outward load tabs, and the hanging part
|
|
// is an outer cup that wraps around the ring. Rigid tabs carry the load. A
|
|
// click latch stops accidental unlocking. Each interface has its own key, so
|
|
// an adapter cannot lock onto the plate.
|
|
|
|
/* [Camera Connector Collar] */
|
|
// Add the bayonet interface to the underside of the exterior plate.
|
|
enable_connector_interface = true; // [true, false]
|
|
// Angle of an optional inner shelf under the collar floor, from horizontal
|
|
// (degrees). 0 = flat floor: the collar ends 2.3 mm below the cable exit,
|
|
// and the floor needs support inside the collar when printed. 35 or 45 print
|
|
// without that support but make the collar 5.1 or 7.3 mm taller.
|
|
connector_shelf_angle = 0; // [0:5:45]
|
|
// Wall thickness of the collar tube (mm).
|
|
connector_wall_t = 4; // [3:0.5:6]
|
|
// Thickness of the collar floor at the bottom ring (mm). The inner shelf above
|
|
// it ties the floor to the wall, so the floor prints without support.
|
|
connector_floor_t = 2; // [1.5:0.5:5]
|
|
// Number of lateral cable exits. A second exit sits opposite the first.
|
|
cable_exit_count = 1; // [1, 2]
|
|
// Width of one lateral cable exit (mm).
|
|
cable_exit_width = 24; // [12:1:30]
|
|
// Depth of one cable exit, down from the collar rim (mm). It also sets the
|
|
// collar height. Route the cables after the collar is locked: each plug goes
|
|
// out through the exit. 10 is the smallest exit an RJ45 plug passes through
|
|
// (latch pressed). The plate tabs set the hard minimum of 7.
|
|
cable_exit_height = 10; // [7:0.5:16]
|
|
// Direction of the first cable exit, from +X (degrees).
|
|
cable_exit_angle = 0; // [0:5:355]
|
|
// Fillet radius on the cable exit corners (mm).
|
|
cable_exit_fillet = 3; // [0:0.5:8]
|
|
|
|
/* [Upper Bayonet - Plate to Collar] */
|
|
// The plate carries a short ring with outward load tabs. The collar wraps
|
|
// around the ring, so the tabs and the screw heads stay hidden when locked.
|
|
// Inner radius of the plate ring (mm). Keep it outside the screw heads.
|
|
ui_ring_r_i = 39.5; // [30:0.5:50]
|
|
// Plate ring wall thickness (mm).
|
|
ui_ring_wall_t = 3; // [2:0.5:6]
|
|
// Outward projection of each plate load tab (mm).
|
|
ui_tab_proj = 4; // [2.5:0.5:8]
|
|
// Height of each plate load tab (mm).
|
|
ui_tab_t = 3; // [2:0.5:6]
|
|
// Height of each collar lug (mm). connector_lock_tab_height.
|
|
ui_lug_t = 3; // [2:0.5:6]
|
|
// Angular width of each collar lug (degrees). connector_lock_tab_width.
|
|
ui_lug_ang = 22; // [10:1:40]
|
|
// Entry gap centers, relative to the cable exit (degrees). The uneven layout
|
|
// lets the collar fit one way only, so its cable exit meets the ring window.
|
|
ui_gap_centers = [0, 90, 180];
|
|
// Angular width of a normal entry gap (degrees).
|
|
ui_gap_ang = 28; // [12:1:60]
|
|
// Angular width of an entry gap that also holds a cable window (degrees).
|
|
ui_gap_wide_ang = 36; // [20:1:70]
|
|
// Lock rotation from insert to locked (degrees). connector_lock_rotation.
|
|
ui_lock_rot = 30; // [15:1:45]
|
|
// Fit clearance per mating surface (mm). connector_lock_clearance.
|
|
ui_clearance = 0.3; // [0.15:0.05:0.5]
|
|
// Gap between the collar rim and the plate face when hanging (mm).
|
|
ui_rim_gap = 0.3; // [0.1:0.05:1]
|
|
// Angular clearance beside the lug at the back stop (degrees).
|
|
ui_seat_clear = 1.5; // [0.5:0.5:4]
|
|
// Angular width of the hard back stop (degrees).
|
|
ui_backstop_w = 3; // [1:0.5:6]
|
|
|
|
/* [Click Latch - Plate to Collar] */
|
|
// A flexible beam in the collar rim snaps over a bump on the plate face at the
|
|
// locked position. The notch wall blocks the reverse turn. Press the release
|
|
// nub toward the floor to unlock.
|
|
// Put the latch right beside the cable exit: the beam starts at the exit
|
|
// edge, and the release nub sits next to the exit. The latch of the lower
|
|
// bayonet copies it, so both clips line up when closed.
|
|
ui_latch_beside_exit = true; // [true, false]
|
|
// Latch position relative to the cable exit (degrees). Used only when
|
|
// ui_latch_beside_exit is false.
|
|
ui_latch_ang = 260; // [0:5:355]
|
|
// Height of the latch bump under the plate face (mm).
|
|
ui_latch_bump_h = 1.2; // [0.8:0.1:2.5]
|
|
// Tangential width of the latch bump (mm).
|
|
ui_latch_bump_w = 3; // [2:0.5:6]
|
|
// Thickness of the flexible latch beam (mm).
|
|
ui_latch_beam_t = 2.6; // [1.5:0.1:4]
|
|
// Height of the slit under the latch beam (mm). It limits the release travel.
|
|
ui_latch_slit_h = 2.8; // [1.5:0.1:4]
|
|
// Beam length from the notch to its anchor (degrees).
|
|
ui_latch_anchor_ang = 20; // [10:1:40]
|
|
// Length of the lead-in ramp at the free end of the beam (mm).
|
|
ui_latch_ramp_len = 5; // [2:0.5:10]
|
|
|
|
/* [Lower Bayonet - Collar to Adapter] */
|
|
// The lower bayonet is a copy of the upper one: the same ring, tabs, key
|
|
// layout, latch and back stops, one level down. A camera adapter therefore
|
|
// locks onto the collar bottom or straight onto the plate, in the same
|
|
// orientation. Change the upper bayonet values to change both.
|
|
// Gap between the ring bottom and the adapter floor (mm).
|
|
li_floor_gap = 1; // [0.5:0.1:3]
|
|
|
|
/* [Camera Adapters] */
|
|
// Base plate thickness of an adapter (mm).
|
|
adapter_plate_t = 4; // [3:0.5:8]
|
|
// Central cable hole through an adapter (mm).
|
|
adapter_central_hole_d = 40; // [24:1:52]
|
|
// Add a generic screw-hole pattern to the blank adapter.
|
|
blank_screw_pattern = true; // [true, false]
|
|
// Spacing of the blank adapter screw pattern (mm).
|
|
blank_screw_spacing = 20; // [8:1:44]
|
|
// Screw hole diameter on the blank adapter (mm).
|
|
blank_screw_d = 3.2; // [2:0.1:5]
|
|
|
|
/* [Tapo Twist Mount] */
|
|
// Male twist-lock that the Tapo camera turns onto: a short boss with 2 lugs
|
|
// at 180 degrees, like the original Tapo base. The camera holds the socket
|
|
// and its own latch. It fits C200, C202, C210, C211, C212, C216, C220, C222,
|
|
// C230, TC70-TC74 and Kasa EC70/EC71. Values come from 11 printable replicas
|
|
// that users report as a good fit; no official drawing exists.
|
|
// Gap from the camera face to the lug underside (mm). The camera ledge sits
|
|
// here. This is the one sensitive value: test 2.7 / 3.0 / 3.3 with
|
|
// tapo_ceiling_tapo_base.scad and keep the one that clicks with no play.
|
|
tapo_lug_gap = 3.0; // [2.4:0.1:3.6]
|
|
// Lug thickness (mm). Keep at least 1.5; the lugs are the weak point.
|
|
tapo_lug_t = 1.5; // [1.2:0.1:2.5]
|
|
// Lug width (mm).
|
|
tapo_lug_w = 7.0; // [6:0.1:8]
|
|
// Lug tip radius from the center (mm). 22 gives 44 mm tip to tip.
|
|
tapo_lug_tip_r = 22.0; // [20:0.1:23]
|
|
// Corner radius at the lug tips (mm).
|
|
tapo_lug_corner_r = 1.0; // [0:0.1:2]
|
|
// Boss diameter at the root and at the top (mm). About 1 mm draft per side.
|
|
tapo_boss_d_root = 30.4; // [27:0.1:33]
|
|
tapo_boss_d_top = 28.4; // [26:0.1:32]
|
|
// Detent rib under each lug tip (mm high). 0 turns it off.
|
|
tapo_detent_h = 0.3; // [0:0.1:0.8]
|
|
// Annular recess in the camera face around the boss (diameter, depth, mm).
|
|
tapo_recess_d = 47; // [40:0.5:52]
|
|
tapo_recess_h = 1.0; // [0:0.1:2]
|
|
// Rotation of the two Tapo wings, relative to the cable exit (degrees). Turn
|
|
// them until the camera connectors face the cable exit once the camera has
|
|
// clicked on. It moves the wings of the direct Tapo adapter and the printed
|
|
// base, and the two screw holes of the original-base adapter (the factory
|
|
// base has its screws at 90 degrees to its wings).
|
|
tapo_wing_angle = 22; // [0:1:179]
|
|
// Central hole through the boss on the direct Tapo adapter (mm).
|
|
tapo_hole_d = 24; // [0:1:26]
|
|
// Printed base (a copy of the original Tapo base, and the gap test):
|
|
// plate diameter, plate thickness, lug windows, screw pitch and holes.
|
|
tapo_base_plate_d = 56; // [50:0.5:62]
|
|
tapo_base_plate_t = 3.5; // [2.5:0.1:5]
|
|
tapo_base_window_w = 8; // [6:0.5:10]
|
|
tapo_base_screw_pitch = 39; // [30:0.5:45]
|
|
tapo_base_screw_d = 4.5; // [3.5:0.1:5.5]
|
|
tapo_base_csk_d = 8.4; // [6:0.1:10]
|
|
// Lug gaps of the three test bases printed by tapo_ceiling_tapo_base.scad.
|
|
tapo_fit_gaps = [2.7, 3.0, 3.3];
|
|
|
|
// ============================================
|
|
// CONNECTOR - BAYONET RECORDS
|
|
// ============================================
|
|
// Both bayonet interfaces use one mechanism:
|
|
// fixed part -> a flat face, a short ring below it with outward load tabs,
|
|
// hard back stops and a latch bump on the face.
|
|
// hanging part -> an outer cup. Its wall wraps around the ring, its inward
|
|
// lugs sit flush with its rim, and its rim carries a latch
|
|
// beam, a groove, a lead-in ramp, a notch and a release nub.
|
|
// Lock = push up, turn by the lock rotation until it clicks. Unlock = press the
|
|
// nub toward the floor, turn back, pull down.
|
|
//
|
|
// Each interface is a record: a list of [key, value] pairs made by
|
|
// bay_iface(). The shared modules read every size and angle from the record.
|
|
|
|
// Value of key k in record I.
|
|
function bay_get(I, k) =
|
|
let (i = search([k], I)[0])
|
|
assert(i != [], str("Unknown bayonet key: ", k))
|
|
I[i][1];
|
|
|
|
// Angle (degrees) of an arc of length mm at radius r.
|
|
function bay_deg(mm, r) = mm / r * 180 / PI;
|
|
|
|
// Make a bayonet record. face_z is the flat face of the fixed part. The cup
|
|
// wall ends at outer_r_o, or is outer_wall_t thick when outer_r_o is undef.
|
|
// Gap and latch angles are relative to the cable exit. The back stops reach
|
|
// down to the ring lower edge, or end at the tab top when stop_on_tab is true.
|
|
function bay_iface(face_z, ring_r_i, ring_wall_t, tab_proj, tab_t, lug_t, lug_ang,
|
|
gap_centers, gap_w, lock_rot, clearance, rim_gap, seat_clear,
|
|
backstop_w, latch_ang, bump_h, bump_w, beam_t, slit_h,
|
|
anchor_ang, ramp_len, outer_wall_t, outer_r_o,
|
|
stop_on_tab = false) =
|
|
let (
|
|
ring_r_o = ring_r_i + ring_wall_t,
|
|
tab_r_o = ring_r_o + tab_proj,
|
|
lug_r_i = ring_r_o + 2 * clearance, // cup lug inner edge
|
|
out_r_i = tab_r_o + clearance, // cup wall rides on the tab tips
|
|
out_r_o = is_undef(outer_r_o) ? out_r_i + outer_wall_t : outer_r_o,
|
|
// When the cup hangs, each lug bottom rests on a tab top.
|
|
rim_z = face_z - rim_gap, // cup rim
|
|
lug_bot_z = rim_z - lug_t, // lugs flush with the rim
|
|
tab_top_z = lug_bot_z - clearance,
|
|
tab_bot_z = tab_top_z - tab_t, // ring lower edge
|
|
// Click latch. Cup frame = fixed frame when locked.
|
|
latch_c = cable_exit_angle + latch_ang,
|
|
latch_rm = (out_r_i + out_r_o) / 2,
|
|
bw = bay_deg(bump_w, latch_rm),
|
|
clr = bay_deg(clearance, latch_rm),
|
|
ramp = bay_deg(ramp_len, latch_rm),
|
|
slitw = bay_deg(1.2, latch_rm),
|
|
free = latch_c - bw / 2 - clr - ramp - 1 // beam free end
|
|
) [
|
|
["face_z", face_z], ["rim_z", rim_z],
|
|
["ring_r_i", ring_r_i], ["ring_wall_t", ring_wall_t], ["ring_r_o", ring_r_o],
|
|
["tab_proj", tab_proj], ["tab_r_o", tab_r_o], ["tab_t", tab_t],
|
|
["tab_top_z", tab_top_z], ["tab_bot_z", tab_bot_z],
|
|
["lug_t", lug_t], ["lug_ang", lug_ang], ["lug_r_i", lug_r_i], ["lug_bot_z", lug_bot_z],
|
|
["out_r_i", out_r_i], ["out_r_o", out_r_o],
|
|
["gaps", [ for (c = gap_centers) cable_exit_angle + c ]], ["gap_w", gap_w],
|
|
["lock_rot", lock_rot], ["clearance", clearance],
|
|
["seat_clear", seat_clear], ["backstop_w", backstop_w],
|
|
["stop_z0", stop_on_tab ? tab_top_z - 0.01 : tab_bot_z], // back stop bottom
|
|
["latch_c", latch_c], ["bump_h", bump_h], ["bw", bw], ["clr", clr],
|
|
["depth", bump_h - rim_gap + clearance], // groove and notch depth
|
|
["beam_t", beam_t], ["slit_h", slit_h],
|
|
["ramp", ramp], ["slitw", slitw], ["free", free],
|
|
["anchor", latch_c + anchor_ang], // beam root
|
|
["g0", latch_c - lock_rot - bw / 2 - 2] // groove start
|
|
];
|
|
|
|
function bay_n(I) = len(bay_get(I, "gaps"));
|
|
function bay_gap(I, i) = bay_get(I, "gaps")[i];
|
|
function bay_gw(I, i) = bay_get(I, "gap_w")[i];
|
|
// The gap after gap i, unwrapped past 360 degrees.
|
|
function bay_next_gap(I, i) = let (j = (i + 1) % bay_n(I))
|
|
bay_gap(I, j) + (j == 0 ? 360 : 0);
|
|
// Locked lug centers. Each lug turns from its gap onto the tab below it.
|
|
function bay_lug_centers(I) = [ for (g = bay_get(I, "gaps")) g - bay_get(I, "lock_rot") ];
|
|
// Tab i spans from gap i to gap i+1.
|
|
function bay_tab_a0(I, i) = bay_gap(I, i) + bay_gw(I, i) / 2;
|
|
function bay_tab_a1(I, i) = bay_next_gap(I, i) - bay_gw(I, (i + 1) % bay_n(I)) / 2;
|
|
// Back stop for the lug that locks onto tab i (on its lock-travel side).
|
|
function bay_backstop_c(I, i) = bay_next_gap(I, i) - bay_get(I, "lock_rot")
|
|
- bay_get(I, "lug_ang") / 2 - bay_get(I, "seat_clear") - bay_get(I, "backstop_w") / 2;
|
|
|
|
// Smallest angle between two directions (degrees).
|
|
function bay_adiff(a, b) = abs(((a - b) % 360 + 540) % 360 - 180);
|
|
// True when the arcs [a0, a1] and [b0, b1] overlap (degrees).
|
|
function bay_overlap(a0, a1, b0, b1) =
|
|
len([ for (k = [-720 : 360 : 720]) if (a0 + k < b1 && b0 < a1 + k) k ]) > 0;
|
|
// True when a lug of width w, centered at angle a, passes an entry gap of R.
|
|
function bay_passes(R, a, w) =
|
|
len([ for (i = [0 : bay_n(R) - 1])
|
|
if (bay_adiff(a, bay_gap(R, i)) <= (bay_gw(R, i) - w) / 2) i ]) > 0;
|
|
// Turns (0.5 degree steps) at which every lug of cup C, turned from its insert
|
|
// position, passes an entry gap of ring R. Used for the keying checks.
|
|
function bay_fit_turns(R, C) = [ for (t = [0 : 0.5 : 359.5])
|
|
if (len([ for (g = bay_get(C, "gaps"))
|
|
if (!bay_passes(R, g + t, bay_get(C, "lug_ang"))) g ]) == 0) t ];
|
|
|
|
// ============================================
|
|
// CONNECTOR - DERIVED VALUES
|
|
// ============================================
|
|
|
|
// Cable exit angles, and the keyed entry gaps of the upper bayonet.
|
|
cable_exit_angles = [ for (k = [0 : cable_exit_count - 1]) cable_exit_angle + k * 180 ];
|
|
function ui_is_exit(c) = c == 0 || (cable_exit_count == 2 && c == 180);
|
|
|
|
// Latch angle beside the cable exit. The beam free end starts just past the
|
|
// exit edge at the cup wall inner face, so no thin wall stays between them.
|
|
lat_r_i = ui_ring_r_i + ui_ring_wall_t + ui_tab_proj + ui_clearance;
|
|
lat_rm = lat_r_i + connector_wall_t / 2;
|
|
lat_edge = asin(cable_exit_width / 2 / lat_r_i);
|
|
ui_latch_ang_eff = ui_latch_beside_exit
|
|
? lat_edge + 0.1 + 1 + bay_deg(ui_latch_ramp_len, lat_rm)
|
|
+ bay_deg(ui_clearance, lat_rm) + bay_deg(ui_latch_bump_w, lat_rm) / 2
|
|
: ui_latch_ang;
|
|
|
|
// Upper bayonet: the plate face (z = 0) is the fixed part, the collar is the cup.
|
|
ui_bay = bay_iface(face_z = 0,
|
|
ring_r_i = ui_ring_r_i, ring_wall_t = ui_ring_wall_t,
|
|
tab_proj = ui_tab_proj, tab_t = ui_tab_t, lug_t = ui_lug_t, lug_ang = ui_lug_ang,
|
|
gap_centers = ui_gap_centers,
|
|
gap_w = [ for (c = ui_gap_centers) ui_is_exit(c) ? ui_gap_wide_ang : ui_gap_ang ],
|
|
lock_rot = ui_lock_rot, clearance = ui_clearance, rim_gap = ui_rim_gap,
|
|
seat_clear = ui_seat_clear, backstop_w = ui_backstop_w,
|
|
latch_ang = ui_latch_ang_eff, bump_h = ui_latch_bump_h, bump_w = ui_latch_bump_w,
|
|
beam_t = ui_latch_beam_t, slit_h = ui_latch_slit_h,
|
|
anchor_ang = ui_latch_anchor_ang, ramp_len = ui_latch_ramp_len,
|
|
outer_wall_t = connector_wall_t);
|
|
|
|
// The lower bayonet copies the upper one. These names keep the collar floor
|
|
// and the adapter cup tied to it.
|
|
li_ring_r_i = ui_ring_r_i;
|
|
li_clearance = ui_clearance;
|
|
li_tab_t = ui_tab_t;
|
|
li_tab_proj = ui_tab_proj;
|
|
|
|
// Collar. It wraps around the plate ring and hangs below the plate.
|
|
connector_r_i = bay_get(ui_bay, "out_r_i");
|
|
connector_r_o = bay_get(ui_bay, "out_r_o");
|
|
conn_top_z = bay_get(ui_bay, "rim_z");
|
|
// The collar ends just below the cable exit: exit depth, a 0.3 mm margin,
|
|
// the optional inner shelf and the floor.
|
|
conn_shelf_h = (connector_r_i - li_ring_r_i) * tan(connector_shelf_angle);
|
|
connector_height = cable_exit_height + 0.3 + conn_shelf_h + connector_floor_t;
|
|
conn_bottom_z = conn_top_z - connector_height;
|
|
// Top of the 45-degree shelf where the collar floor meets the wall.
|
|
conn_shelf_top_z = conn_bottom_z + connector_floor_t + conn_shelf_h;
|
|
|
|
// Lower bayonet: the collar bottom face is the fixed part, the adapter is the
|
|
// cup. It is the upper bayonet one level down, so the adapter cup also fits
|
|
// the plate ring. The adapter outer diameter equals the collar outer diameter.
|
|
li_bay = bay_iface(face_z = conn_bottom_z,
|
|
ring_r_i = ui_ring_r_i, ring_wall_t = ui_ring_wall_t,
|
|
tab_proj = ui_tab_proj, tab_t = ui_tab_t, lug_t = ui_lug_t, lug_ang = ui_lug_ang,
|
|
gap_centers = ui_gap_centers,
|
|
gap_w = [ for (c = ui_gap_centers) ui_is_exit(c) ? ui_gap_wide_ang : ui_gap_ang ],
|
|
lock_rot = ui_lock_rot, clearance = ui_clearance, rim_gap = ui_rim_gap,
|
|
seat_clear = ui_seat_clear, backstop_w = ui_backstop_w,
|
|
latch_ang = ui_latch_ang_eff, bump_h = ui_latch_bump_h, bump_w = ui_latch_bump_w,
|
|
beam_t = ui_latch_beam_t, slit_h = ui_latch_slit_h,
|
|
anchor_ang = ui_latch_anchor_ang, ramp_len = ui_latch_ramp_len,
|
|
outer_r_o = connector_r_o);
|
|
|
|
// Adapter z-levels. The floor inside the cup is the top of the base plate.
|
|
adapter_rim_z = bay_get(li_bay, "rim_z");
|
|
adapter_top_z = bay_get(li_bay, "tab_bot_z") - li_floor_gap;
|
|
adapter_bot_z = adapter_top_z - adapter_plate_t; // camera face
|
|
// Inner wall of the cup, just inside the collar ring, and the cable funnel
|
|
// that widens at 45 degrees from the central hole up to the rim.
|
|
adapter_spigot_r = li_ring_r_i - 2 * li_clearance;
|
|
adapter_funnel_r = adapter_central_hole_d / 2 + (adapter_rim_z - adapter_bot_z);
|
|
// 45-degree fillets in the floor corners of the ring channel. They shorten
|
|
// the channel floor bridge when the cup prints rim-down. The outer one stays
|
|
// clear of the full-height back stops on the ring, so the cup fits both the
|
|
// collar ring and the plate ring.
|
|
adapter_fillet_out = max(0, min(li_floor_gap + li_clearance + bay_cham(li_tab_t, li_tab_proj) - 1,
|
|
li_floor_gap + (bay_get(li_bay, "out_r_i") - bay_get(li_bay, "tab_r_o"))
|
|
- li_clearance));
|
|
adapter_fillet_in = max(0, li_floor_gap + 2 * li_clearance - 1);
|
|
|
|
// ============================================
|
|
// CONNECTOR - VALIDATION
|
|
// ============================================
|
|
|
|
// Checks that apply to every bayonet record.
|
|
module bay_validate(I, name) {
|
|
n = bay_n(I);
|
|
lug = bay_get(I, "lug_ang");
|
|
rot = bay_get(I, "lock_rot");
|
|
gw = bay_get(I, "gap_w");
|
|
assert(lug + 2 < min(gw),
|
|
str(name, ": a lug is wider than an entry gap. Reduce the lug angle or widen the gaps."));
|
|
assert(rot >= (max(gw) + lug) / 2,
|
|
str(name, ": the lock turn is too small; a lug does not land fully on its tab."));
|
|
assert(min([ for (i = [0 : n - 1]) bay_backstop_c(I, i)
|
|
- bay_get(I, "backstop_w") / 2 - bay_tab_a0(I, i) ]) > 0,
|
|
str(name, ": a back stop falls off its tab. Space the gaps wider or lower the lock turn."));
|
|
assert(rot > bay_get(I, "latch_c") - bay_get(I, "free")
|
|
+ bay_get(I, "slitw") + bay_get(I, "bw") / 2,
|
|
str(name, ": the latch groove is too short for the lock turn. Shorten the ramp or raise the lock turn."));
|
|
assert(bay_get(I, "depth") < bay_get(I, "beam_t") - 0.8,
|
|
str(name, ": the latch notch is too deep for the beam. Raise the beam thickness."));
|
|
assert(len([ for (a = bay_lug_centers(I))
|
|
if (bay_overlap(a - lug / 2, a + lug / 2,
|
|
bay_get(I, "g0") - 1, bay_get(I, "anchor") + 1)) a ]) == 0,
|
|
str(name, ": the latch cuts reach a lug. Move the latch angle."));
|
|
assert(len([ for (t = bay_fit_turns(I, I))
|
|
if (bay_adiff(t, 0) > (max(gw) - lug) / 2) t ]) == 0,
|
|
str(name, ": the cup fits more than one way. Space the entry gaps unevenly."));
|
|
assert(bay_get(I, "out_r_o") - bay_get(I, "out_r_i") >= 3,
|
|
str(name, ": the cup wall is too thin for the latch beam."));
|
|
}
|
|
|
|
bay_validate(ui_bay, "Upper bayonet");
|
|
bay_validate(li_bay, "Lower bayonet");
|
|
assert(!ui_latch_beside_exit ||
|
|
(bay_get(ui_bay, "free") >= asin(cable_exit_width / 2 / bay_get(ui_bay, "out_r_i")) &&
|
|
bay_get(ui_bay, "free") - bay_get(ui_bay, "slitw")
|
|
<= asin(cable_exit_width / 2 / bay_get(ui_bay, "out_r_o")) + 0.05),
|
|
"The latch beside the cable exit leaves a thin wall or cuts the beam. Set ui_latch_beside_exit = false.");
|
|
|
|
assert(ui_ring_r_i > screw_r + screw_head_recess_d / 2,
|
|
"Plate ring covers the screw heads. Raise ui_ring_r_i.");
|
|
assert(2 * asin(cable_exit_width / 2 / bay_get(ui_bay, "ring_r_o")) <= ui_gap_wide_ang - 2,
|
|
"Cable window is wider than its entry gap. Raise ui_gap_wide_ang.");
|
|
assert(len(bay_fit_turns(ui_bay, li_bay)) > 0,
|
|
"The adapter key no longer fits the plate ring.");
|
|
assert(2 * li_ring_r_i >= 40,
|
|
"The collar bottom ring leaves less than 40 mm for the cables. Raise li_ring_r_i.");
|
|
assert(adapter_funnel_r < adapter_spigot_r - 1.5,
|
|
"The adapter cable funnel cuts through its inner wall. Reduce adapter_central_hole_d.");
|
|
assert(conn_top_z - cable_exit_height > conn_shelf_top_z + 0.2,
|
|
"The cable exit reaches the collar floor. Reduce cable_exit_height.");
|
|
assert(bay_get(ui_bay, "tab_bot_z") >= conn_shelf_top_z + 1 - 1e-6,
|
|
"The collar is too short for both bayonets. Raise cable_exit_height.");
|
|
|
|
echo(str("Collar: outer diameter ", 2 * connector_r_o, " mm; hangs ", -conn_bottom_z,
|
|
" mm below the plate"));
|
|
echo(str("Adapter: height ", adapter_rim_z - adapter_bot_z, " mm; camera face ",
|
|
-adapter_bot_z, " mm below the plate"));
|
|
|
|
// ============================================
|
|
// CONNECTOR - HELPER MODULES
|
|
// ============================================
|
|
|
|
// A solid arc block between two radii, over an angular span, extruded in z.
|
|
module bay_arc(r_in, r_out, ang_c, ang_w, z0, h) {
|
|
n = max(2, ceil(ang_w));
|
|
fan = [ for (k = [0 : n]) let (t = ang_c - ang_w/2 + ang_w * k / n)
|
|
[ (r_out + 2) * cos(t), (r_out + 2) * sin(t) ] ];
|
|
translate([0, 0, z0])
|
|
linear_extrude(h)
|
|
intersection() {
|
|
difference() { circle(r_out); circle(r_in); }
|
|
polygon(concat([[0, 0]], fan));
|
|
}
|
|
}
|
|
|
|
// A simple ring wall.
|
|
module ring_wall(r_i, wall_t, z0, h) {
|
|
translate([0, 0, z0])
|
|
linear_extrude(h)
|
|
difference() { circle(r_i + wall_t); circle(r_i); }
|
|
}
|
|
|
|
// A tall angular sector prism, for cutting an arc from a ring of revolution.
|
|
module sector_prism(ang_c, ang_w, R = 200) {
|
|
n = max(2, ceil(ang_w / 1.5));
|
|
translate([0, 0, -500])
|
|
linear_extrude(1000)
|
|
polygon(concat([[0, 0]],
|
|
[ for (k = [0 : n]) let (t = ang_c - ang_w/2 + ang_w * k / n)
|
|
[ R * cos(t), R * sin(t) ] ]));
|
|
}
|
|
|
|
// An arc segment from an (r, z) profile, limited to an angular sector.
|
|
module profile_arc(pts, ang_c, ang_w) {
|
|
intersection() {
|
|
rotate_extrude() polygon(pts);
|
|
sector_prism(ang_c, ang_w);
|
|
}
|
|
}
|
|
|
|
// Self-supporting chamfer size for a tab of the given height and projection.
|
|
function bay_cham(h, proj) = min(h * 0.85, proj * 0.55);
|
|
|
|
// A load lug, chamfered on one face at its free outer edge for print support.
|
|
// The load face stays flat. cham_bottom chamfers the lower face, else the upper.
|
|
module bay_lug(neck, proj, z0, h, ang_c, ang_w, cham_bottom) {
|
|
r_in = neck - 0.01;
|
|
r_out = neck + proj;
|
|
c = bay_cham(h, proj);
|
|
pts = cham_bottom
|
|
? [[r_in, z0], [r_in, z0 + h], [r_out, z0 + h], [r_out, z0 + c], [r_out - c, z0]]
|
|
: [[r_in, z0], [r_in, z0 + h], [r_out - c, z0 + h], [r_out, z0 + h - c], [r_out, z0]];
|
|
profile_arc(pts, ang_c, ang_w);
|
|
}
|
|
|
|
// Fixed half of a bayonet: a short ring under the face with outward load tabs,
|
|
// hard back stops and the latch bump. It is additive. The cup wraps around
|
|
// it, so none of it shows when the cup is locked.
|
|
module bay_ring(I) {
|
|
fz = bay_get(I, "face_z");
|
|
zb = bay_get(I, "tab_bot_z");
|
|
z0 = bay_get(I, "stop_z0");
|
|
r_o = bay_get(I, "ring_r_o");
|
|
ring_wall(bay_get(I, "ring_r_i"), bay_get(I, "ring_wall_t"), zb, fz - zb + 0.01);
|
|
for (i = [0 : bay_n(I) - 1]) {
|
|
a0 = bay_tab_a0(I, i);
|
|
a1 = bay_tab_a1(I, i);
|
|
// Load tab. Its top face carries the cup lug.
|
|
bay_lug(r_o, bay_get(I, "tab_proj"), zb, bay_get(I, "tab_t"),
|
|
(a0 + a1) / 2, a1 - a0, true);
|
|
// Hard back stop on the lock-travel side of the locked lug.
|
|
bay_arc(r_o - 0.01, bay_get(I, "tab_r_o"), bay_backstop_c(I, i),
|
|
bay_get(I, "backstop_w"), z0, fz - z0 + 0.01);
|
|
}
|
|
// Latch bump. It clicks into the notch of the cup latch beam.
|
|
bay_arc(bay_get(I, "out_r_i") + 0.3, bay_get(I, "out_r_o") - 1.5,
|
|
bay_get(I, "latch_c"), bay_get(I, "bw"),
|
|
fz - bay_get(I, "bump_h"), bay_get(I, "bump_h") + 0.01);
|
|
}
|
|
|
|
// Inward lugs of the cup, flush with its rim. The load face is the lug bottom.
|
|
module bay_lugs(I) {
|
|
for (a = bay_lug_centers(I))
|
|
bay_arc(bay_get(I, "lug_r_i"), bay_get(I, "out_r_i") + 0.01, a,
|
|
bay_get(I, "lug_ang"), bay_get(I, "lug_bot_z"), bay_get(I, "lug_t"));
|
|
}
|
|
|
|
// Latch cuts in the cup rim: bump groove, beam slits, notch and ramp.
|
|
module bay_latch_cuts(I) {
|
|
r_i = bay_get(I, "out_r_i");
|
|
r_o = bay_get(I, "out_r_o");
|
|
r_in = r_i - 0.5; // open toward the inside of the cup
|
|
r_out = r_o - 1; // keep an outer skin on the rim
|
|
zt = bay_get(I, "rim_z");
|
|
zb = zt - bay_get(I, "beam_t") - bay_get(I, "slit_h");
|
|
depth = bay_get(I, "depth");
|
|
free = bay_get(I, "free");
|
|
slitw = bay_get(I, "slitw");
|
|
anchor = bay_get(I, "anchor");
|
|
g0 = bay_get(I, "g0");
|
|
ramp = bay_get(I, "ramp");
|
|
// Groove: the bump passes here while the cup is pushed up and turned.
|
|
bay_arc(r_in, r_out, (g0 + free - slitw) / 2, free - slitw - g0,
|
|
zt - depth, depth + 1);
|
|
// Slit at the free end of the beam, and slit under the beam.
|
|
bay_arc(r_i - 1, r_o + 2, free - slitw / 2, slitw, zb, zt - zb + 1);
|
|
bay_arc(r_i - 1, r_o + 2, (free - slitw + anchor) / 2, anchor - free + slitw,
|
|
zb, bay_get(I, "slit_h"));
|
|
// Notch. Its steep walls stop the reverse turn.
|
|
bay_arc(r_in, r_out, bay_get(I, "latch_c"), bay_get(I, "bw") + 2 * bay_get(I, "clr"),
|
|
zt - depth, depth + 1);
|
|
// Lead-in ramp on the free end, so the bump lifts the beam smoothly. Its
|
|
// first slice overlaps the slit a little, so no zero-thickness wall remains.
|
|
hull() {
|
|
bay_arc(r_in, r_out, free + 0.095, 0.21, zt - depth, depth + 1);
|
|
bay_arc(r_in, r_out, free + ramp - 0.1, 0.2, zt, 1);
|
|
}
|
|
}
|
|
|
|
// Release nub on the outside of the latch beam, near its free end.
|
|
module bay_latch_nub(I) {
|
|
r_o = bay_get(I, "out_r_o");
|
|
bay_arc(r_o - 0.01, r_o + 1.5, bay_get(I, "free") + 4.5, 6,
|
|
bay_get(I, "rim_z") - bay_get(I, "beam_t"), bay_get(I, "beam_t"));
|
|
}
|
|
|
|
// Plate bayonet ring with cable windows aligned with the collar exits. It is
|
|
// additive; the plate unions it.
|
|
module plate_bayonet_ring() {
|
|
zb = bay_get(ui_bay, "tab_bot_z");
|
|
difference() {
|
|
bay_ring(ui_bay);
|
|
for (e = cable_exit_angles)
|
|
rotate([0, 0, e])
|
|
translate([ui_ring_r_i - 1, -cable_exit_width / 2, zb - 1])
|
|
cube([ui_ring_wall_t + 2, cable_exit_width, -zb + 1.02]);
|
|
}
|
|
}
|
|
|
|
// Adapter cup body. The outer wall wraps around the collar bottom ring. An
|
|
// inner wall fills the center down to a 45-degree cable funnel, so the cup
|
|
// prints rim-down: the floor over the ring channel is a short bridge.
|
|
module adapter_body(hole_d = adapter_central_hole_d) {
|
|
h = adapter_rim_z - adapter_bot_z;
|
|
t = adapter_top_z - adapter_bot_z;
|
|
rh = hole_d / 2;
|
|
ri = bay_get(li_bay, "out_r_i");
|
|
fo = adapter_fillet_out;
|
|
fi = adapter_fillet_in;
|
|
translate([0, 0, adapter_bot_z])
|
|
difference() {
|
|
cylinder(r = connector_r_o, h = h);
|
|
// Channel for the collar bottom ring and its tabs, less the fillets.
|
|
difference() {
|
|
ring_wall(adapter_spigot_r, ri - adapter_spigot_r, t, h);
|
|
translate([0, 0, t - 0.01])
|
|
difference() {
|
|
cylinder(r = ri + 1, h = fo + 0.01);
|
|
cylinder(r1 = ri - fo - 0.01, r2 = ri, h = fo + 0.01);
|
|
}
|
|
translate([0, 0, t - 0.01])
|
|
cylinder(r1 = adapter_spigot_r + fi + 0.01, r2 = adapter_spigot_r, h = fi + 0.01);
|
|
}
|
|
// Cable funnel: the central hole at the camera face, 45 degrees up.
|
|
translate([0, 0, -0.01])
|
|
cylinder(r1 = rh - 0.01, r2 = rh + h + 0.01, h = h + 0.02);
|
|
}
|
|
}
|
|
|
|
// Shared adapter core: the cup with its lugs and click latch, and the central
|
|
// cable hole. It locks onto the collar bottom ring. Camera features attach to
|
|
// the base plate below it. Built in assembly coordinates.
|
|
module adapter_core(hole_d = adapter_central_hole_d) {
|
|
difference() {
|
|
union() {
|
|
adapter_body(hole_d);
|
|
bay_lugs(li_bay);
|
|
bay_latch_nub(li_bay);
|
|
}
|
|
bay_latch_cuts(li_bay);
|
|
}
|
|
}
|
|
|
|
// Lateral cable exit cutters through the collar wall. Each exit is open at
|
|
// the rim; the plate face closes it when the collar is locked.
|
|
module cable_slot() {
|
|
h = cable_exit_height + 4;
|
|
for (e = cable_exit_angles)
|
|
rotate([0, 0, e])
|
|
translate([connector_r_i - 1, 0, conn_top_z - cable_exit_height + h / 2])
|
|
rotate([0, 90, 0])
|
|
linear_extrude(connector_wall_t + 3)
|
|
offset(r = cable_exit_fillet)
|
|
offset(delta = -cable_exit_fillet)
|
|
square([h, cable_exit_width], center = true);
|
|
}
|
|
|
|
// ============================================
|
|
// TAPO TWIST MOUNT
|
|
// ============================================
|
|
|
|
// The male Tapo twist mount, in a local frame: z = 0 is the face the camera
|
|
// rests on, +z points into the camera. The boss starts at the recess floor.
|
|
// hole_d > 0 leaves a hole through the boss; the lugs then start at its edge.
|
|
module tapo_male(gap = tapo_lug_gap, hole_d = 0) {
|
|
top = gap + tapo_lug_t;
|
|
r_in = max(0, hole_d / 2);
|
|
difference() {
|
|
union() {
|
|
translate([0, 0, -tapo_recess_h])
|
|
cylinder(r1 = tapo_boss_d_root / 2, r2 = tapo_boss_d_top / 2,
|
|
h = top + tapo_recess_h);
|
|
for (a = [0, 180] + [1, 1] * (cable_exit_angle + tapo_wing_angle))
|
|
rotate([0, 0, a]) {
|
|
// Lug. Its underside holds the camera ledge.
|
|
translate([0, 0, gap])
|
|
linear_extrude(tapo_lug_t)
|
|
// Clipped to the tip radius, so no corner sweeps
|
|
// wider than tapo_lug_tip_r when the camera turns.
|
|
intersection() {
|
|
hull() {
|
|
translate([r_in, -tapo_lug_w / 2]) square([0.01, tapo_lug_w]);
|
|
for (s = [-1, 1])
|
|
translate([tapo_lug_tip_r - tapo_lug_corner_r,
|
|
s * (tapo_lug_w / 2 - tapo_lug_corner_r)])
|
|
circle(r = max(0.01, tapo_lug_corner_r));
|
|
}
|
|
circle(r = tapo_lug_tip_r);
|
|
}
|
|
// Detent rib under the lug tip.
|
|
if (tapo_detent_h > 0)
|
|
translate([tapo_lug_tip_r - 2, -0.5, gap - tapo_detent_h])
|
|
cube([1.5, 1, tapo_detent_h + 0.01]);
|
|
}
|
|
}
|
|
if (hole_d > 0)
|
|
translate([0, 0, -tapo_recess_h - 1])
|
|
cylinder(d = hole_d, h = top + tapo_recess_h + 2);
|
|
}
|
|
}
|
|
|
|
// Recess cut into the face around the boss, in the same local frame.
|
|
module tapo_recess_cut() {
|
|
if (tapo_recess_h > 0)
|
|
translate([0, 0, -tapo_recess_h])
|
|
difference() {
|
|
cylinder(d = tapo_recess_d, h = tapo_recess_h + 0.01);
|
|
translate([0, 0, -1]) cylinder(d = tapo_boss_d_root - 0.01, h = tapo_recess_h + 2);
|
|
}
|
|
}
|
|
|
|
// This file defines parameters and modules only. When it is opened on its own
|
|
// it renders a micro marker so batch STL tools do not fail on an empty object.
|
|
// Every part file sets `parameters_only = false` before the include, which
|
|
// removes the marker from that part.
|
|
if (is_undef(parameters_only) ? true : parameters_only) cube(0.001);
|