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