3d-printing/tapo-ceiling-mount/tapo_ceiling_mount_parameters.scad

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