// ============================================================ // Cable Management Tray — Issue #628 // Agent : sl-mechanical // Date : 2026-03-15 // Part catalogue: // 1. tray_body — under-plate tray with snap-in cable channels, Velcro // tie-down slots every 40 mm, pass-through holes, label slots // 2. tnut_bracket — 2020 T-nut rail mount bracket (×2, slide under tray) // 3. channel_clip — snap-in divider clip separating power / signal / servo zones // 4. cover_panel — hinged snap-on lid (living-hinge PETG flexure strip) // 5. cable_saddle — individual cable saddle / strain-relief clip (×n) // // BOM: // 4 × M5×10 BHCS + M5 T-nuts (tnut_bracket × 2 to rail) // 4 × M3×8 SHCS (tnut_bracket to tray body) // n × 100 mm Velcro tie-down strips (through 6×2 mm slots, every 40 mm) // // Cable channel layout (X axis, inside tray): // Zone A — Power (2S–6S LiPo, XT60/XT30): 20 mm wide, 14 mm deep // Zone B — Signal (JST-SH, PWM, I2C, UART): 14 mm wide, 10 mm deep // Zone C — Servo (JST-PH, thick servo leads): 14 mm wide, 12 mm deep // Divider walls: 2.5 mm thick between zones // // Print settings (PETG): // tray_body / tnut_bracket / channel_clip : 5 perimeters, 40 % gyroid, no supports // cover_panel : 3 perimeters, 20 % gyroid, no supports // (living-hinge — print flat, thin strip flexes) // cable_saddle : 3 perimeters, 30 % gyroid, no supports // // Export commands: // openscad -D 'RENDER="tray_body"' -o tray_body.stl cable_tray.scad // openscad -D 'RENDER="tnut_bracket"' -o tnut_bracket.stl cable_tray.scad // openscad -D 'RENDER="channel_clip"' -o channel_clip.stl cable_tray.scad // openscad -D 'RENDER="cover_panel"' -o cover_panel.stl cable_tray.scad // openscad -D 'RENDER="cable_saddle"' -o cable_saddle.stl cable_tray.scad // openscad -D 'RENDER="assembly"' -o assembly.png cable_tray.scad // ============================================================ RENDER = "assembly"; // tray_body | tnut_bracket | channel_clip | cover_panel | cable_saddle | assembly $fn = 48; EPS = 0.01; // ── 2020 rail constants ────────────────────────────────────── RAIL_W = 20.0; TNUT_W = 9.8; TNUT_H = 5.5; TNUT_L = 12.0; SLOT_NECK_H = 3.2; M5_D = 5.2; M5_HEAD_D = 9.5; M5_HEAD_H = 4.0; // ── Tray geometry ──────────────────────────────────────────── TRAY_L = 280.0; // length along rail (7 × 40 mm tie-down pitch) TRAY_W = 60.0; // width across rail (covers standard 40 mm rail pair) TRAY_WALL = 2.5; // side / floor wall thickness TRAY_DEPTH = 18.0; // interior depth (tallest zone + wall) // Cable channel zones (widths must sum to TRAY_W - 2*TRAY_WALL - 2*DIV_T) DIV_T = 2.5; // divider wall thickness ZONE_A_W = 20.0; // Power ZONE_A_D = 14.0; ZONE_B_W = 14.0; // Signal ZONE_B_D = 10.0; ZONE_C_W = 14.0; // Servo ZONE_C_D = 12.0; // Total inner width used: ZONE_A_W + ZONE_B_W + ZONE_C_W + 2*DIV_T = 55 mm < TRAY_W - 2*TRAY_WALL = 55 mm ✓ // Tie-down slots (Velcro strips) TIEDOWN_PITCH = 40.0; TIEDOWN_W = 6.0; // slot width (fits 6 mm wide Velcro) TIEDOWN_T = 2.2; // slot through-thickness (floor) TIEDOWN_CNT = 7; // 7 positions along tray // Pass-through holes in floor PASSTHRU_D = 12.0; // circular grommet-compatible pass-through PASSTHRU_CNT = 3; // one per zone, at tray mid-length // Label slots (rear outer wall) LABEL_W = 24.0; LABEL_H = 8.0; LABEL_T = 1.0; // depth from outer face // Snap ledge for cover SNAP_LEDGE_H = 2.5; SNAP_LEDGE_D = 1.5; // ── T-nut bracket ──────────────────────────────────────────── BKT_L = 60.0; BKT_W = TRAY_W; BKT_T = 6.0; BOLT_PITCH = 40.0; M3_D = 3.2; M3_HEAD_D = 6.0; M3_HEAD_H = 3.0; M3_NUT_W = 5.5; M3_NUT_H = 2.4; // ── Cover panel ────────────────────────────────────────────── CVR_T = 1.8; // panel thickness HINGE_T = 0.6; // living-hinge strip thickness (printed in PETG) HINGE_W = 3.0; // hinge strip width (flexes easily) SNAP_HOOK_H = 3.5; // snap hook height SNAP_HOOK_T = 2.2; // ── Cable saddle ───────────────────────────────────────────── SAD_W = 12.0; SAD_H = 8.0; SAD_T = 2.5; SAD_BORE_D = 7.0; // cable bundle bore SAD_CLIP_T = 1.6; // snap arm thickness // ── Utilities ──────────────────────────────────────────────── module chamfer_cube(size, ch=1.0) { hull() { translate([ch, ch, 0]) cube([size[0]-2*ch, size[1]-2*ch, EPS]); translate([0, 0, ch]) cube(size - [0, 0, ch]); } } module hex_pocket(af, depth) { cylinder(d=af/cos(30), h=depth, $fn=6); } // ── Part 1: tray_body ─────────────────────────────────────── module tray_body() { difference() { // Outer shell union() { chamfer_cube([TRAY_L, TRAY_W, TRAY_DEPTH + TRAY_WALL], ch=1.5); // Snap ledge along top of both long walls (for cover_panel) for (y = [-SNAP_LEDGE_D, TRAY_W]) translate([0, y, TRAY_DEPTH]) cube([TRAY_L, TRAY_WALL + SNAP_LEDGE_D, SNAP_LEDGE_H]); } // Interior cavity translate([TRAY_WALL, TRAY_WALL, TRAY_WALL]) cube([TRAY_L - 2*TRAY_WALL, TRAY_W - 2*TRAY_WALL, TRAY_DEPTH + EPS]); // ── Zone dividers (subtract from solid to leave walls) ── // Zone A (Power) inner floor cut — full depth A translate([TRAY_WALL, TRAY_WALL, TRAY_WALL + (TRAY_DEPTH - ZONE_A_D)]) cube([TRAY_L - 2*TRAY_WALL, ZONE_A_W, ZONE_A_D + EPS]); // Zone B (Signal) inner floor cut translate([TRAY_WALL, TRAY_WALL + ZONE_A_W + DIV_T, TRAY_WALL + (TRAY_DEPTH - ZONE_B_D)]) cube([TRAY_L - 2*TRAY_WALL, ZONE_B_W, ZONE_B_D + EPS]); // Zone C (Servo) inner floor cut translate([TRAY_WALL, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T, TRAY_WALL + (TRAY_DEPTH - ZONE_C_D)]) cube([TRAY_L - 2*TRAY_WALL, ZONE_C_W, ZONE_C_D + EPS]); // ── Velcro tie-down slots (floor, every 40 mm) ────────── for (i = [0:TIEDOWN_CNT-1]) { x = TRAY_WALL + 20 + i * TIEDOWN_PITCH - TIEDOWN_W/2; // Zone A slot translate([x, TRAY_WALL + 2, -EPS]) cube([TIEDOWN_W, ZONE_A_W - 4, TRAY_WALL + 2*EPS]); // Zone B slot translate([x, TRAY_WALL + ZONE_A_W + DIV_T + 2, -EPS]) cube([TIEDOWN_W, ZONE_B_W - 4, TRAY_WALL + 2*EPS]); // Zone C slot translate([x, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + 2, -EPS]) cube([TIEDOWN_W, ZONE_C_W - 4, TRAY_WALL + 2*EPS]); } // ── Pass-through holes in floor (centre of each zone at mid-length) ── mid_x = TRAY_L / 2; // Zone A translate([mid_x, TRAY_WALL + ZONE_A_W/2, -EPS]) cylinder(d=PASSTHRU_D, h=TRAY_WALL + 2*EPS); // Zone B translate([mid_x, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W/2, -EPS]) cylinder(d=PASSTHRU_D, h=TRAY_WALL + 2*EPS); // Zone C translate([mid_x, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + ZONE_C_W/2, -EPS]) cylinder(d=PASSTHRU_D, h=TRAY_WALL + 2*EPS); // ── Label slots on front wall (y = 0) — one per zone ──── zone_ctrs = [TRAY_WALL + ZONE_A_W/2, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W/2, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + ZONE_C_W/2]; label_z = TRAY_WALL + 2; for (yc = zone_ctrs) translate([TRAY_L/2 - LABEL_W/2, -EPS, label_z]) cube([LABEL_W, LABEL_T + EPS, LABEL_H]); // ── M3 bracket bolt holes in floor (4 corners) ────────── for (x = [20, TRAY_L - 20]) for (y = [TRAY_W/4, 3*TRAY_W/4]) translate([x, y, -EPS]) cylinder(d=M3_D, h=TRAY_WALL + 2*EPS); // ── Channel clip snap sockets (top of each divider, every 80 mm) ── for (i = [0:2]) { cx = 40 + i * 80; for (dy = [ZONE_A_W, ZONE_A_W + DIV_T + ZONE_B_W]) translate([cx - 3, TRAY_WALL + dy - 1, TRAY_DEPTH - 4]) cube([6, DIV_T + 2, 4 + EPS]); } } // ── Divider walls (positive geometry) ─────────────────── // Wall between Zone A and Zone B translate([TRAY_WALL, TRAY_WALL + ZONE_A_W, TRAY_WALL]) cube([TRAY_L - 2*TRAY_WALL, DIV_T, TRAY_DEPTH - ZONE_A_D]); // partial height — lower in A zone // Wall between Zone B and Zone C translate([TRAY_WALL, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W, TRAY_WALL]) cube([TRAY_L - 2*TRAY_WALL, DIV_T, TRAY_DEPTH - ZONE_B_D]); } // ── Part 2: tnut_bracket ──────────────────────────────────── module tnut_bracket() { difference() { chamfer_cube([BKT_L, BKT_W, BKT_T], ch=1.5); // M5 T-nut holes (2 per bracket, on rail centreline) for (x = [BKT_L/2 - BOLT_PITCH/2, BKT_L/2 + BOLT_PITCH/2]) { translate([x, BKT_W/2, -EPS]) { cylinder(d=M5_D, h=BKT_T + 2*EPS); cylinder(d=M5_HEAD_D, h=M5_HEAD_H + EPS); } translate([x - TNUT_L/2, BKT_W/2 - TNUT_W/2, BKT_T - TNUT_H]) cube([TNUT_L, TNUT_W, TNUT_H + EPS]); } // M3 tray-attachment holes (4 corners) for (x = [10, BKT_L - 10]) for (y = [10, BKT_W - 10]) { translate([x, y, -EPS]) cylinder(d=M3_D, h=BKT_T + 2*EPS); // M3 hex nut captured pocket (from top) translate([x, y, BKT_T - M3_NUT_H - 0.2]) hex_pocket(M3_NUT_W + 0.3, M3_NUT_H + 0.3); } // Weight relief translate([15, 8, -EPS]) cube([BKT_L - 30, BKT_W - 16, BKT_T/2]); } } // ── Part 3: channel_clip ──────────────────────────────────── // Snap-in clip that locks into divider-wall snap sockets; // holds individual bundles in their zone and acts as colour-coded zone marker. module channel_clip() { clip_body_w = 6.0; clip_body_h = DIV_T + 4.0; clip_body_t = 8.0; tab_h = 3.5; tab_w = 2.5; difference() { union() { // Body spanning divider cube([clip_body_t, clip_body_w, clip_body_h]); // Snap tabs (bottom, straddle divider) for (s = [0, clip_body_w - tab_w]) translate([clip_body_t/2 - 1, s, -tab_h]) cube([2, tab_w, tab_h + 1]); } // Cable radius slot on each face translate([-EPS, clip_body_w/2, clip_body_h * 0.6]) rotate([0, 90, 0]) cylinder(d=5.0, h=clip_body_t + 2*EPS); // Snap tab undercut for flex for (s = [0, clip_body_w - tab_w]) translate([clip_body_t/2 - 2, s - EPS, -tab_h + 1.5]) cube([4, tab_w + 2*EPS, 1.5]); } } // ── Part 4: cover_panel ───────────────────────────────────── // Flat snap-on lid with living-hinge along one long edge. // Print flat; PETG living hinge flexes ~90° to snap onto tray. module cover_panel() { total_w = TRAY_W + 2 * SNAP_HOOK_T; difference() { union() { // Main panel cube([TRAY_L, TRAY_W, CVR_T]); // Living hinge strip along back edge (thin, flexes) translate([0, TRAY_W - EPS, 0]) cube([TRAY_L, HINGE_W, HINGE_T]); // Snap hooks along front edge (clips under tray snap ledge) for (x = [20, TRAY_L/2 - 20, TRAY_L/2 + 20, TRAY_L - 20]) translate([x - SNAP_HOOK_T/2, -SNAP_HOOK_H + EPS, 0]) difference() { cube([SNAP_HOOK_T, SNAP_HOOK_H, CVR_T + 1.5]); // Hook nose chamfer translate([-EPS, -EPS, CVR_T]) rotate([0, 0, 0]) cube([SNAP_HOOK_T + 2*EPS, 1.5, 1.5]); } } // Ventilation slots (3 rows × 6 slots) for (row = [0:2]) for (col = [0:5]) { sx = 20 + col * 40 + row * 10; sy = 10 + row * 12; if (sx + 25 < TRAY_L && sy + 6 < TRAY_W) translate([sx, sy, -EPS]) cube([25, 6, CVR_T + 2*EPS]); } // Zone label windows (align with tray label slots) zone_ctrs = [TRAY_WALL + ZONE_A_W/2, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W/2, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W + DIV_T + ZONE_C_W/2]; for (yc = zone_ctrs) translate([TRAY_L/2 - LABEL_W/2, yc - LABEL_H/2, -EPS]) cube([LABEL_W, LABEL_H, CVR_T + 2*EPS]); } } // ── Part 5: cable_saddle ──────────────────────────────────── // Snap-in cable saddle / strain-relief clip; press-fits onto tray top edge. module cable_saddle() { arm_gap = TRAY_WALL + 0.4; // fits over tray wall arm_len = 8.0; difference() { union() { // Body chamfer_cube([SAD_W, SAD_T * 2 + arm_gap, SAD_H], ch=1.0); // Cable retaining arch translate([SAD_W/2, SAD_T + arm_gap/2, SAD_H]) scale([1, 0.6, 1]) difference() { cylinder(d=SAD_BORE_D + SAD_CLIP_T * 2, h=SAD_T); translate([0, 0, -EPS]) cylinder(d=SAD_BORE_D, h=SAD_T + 2*EPS); translate([-SAD_BORE_D, 0, -EPS]) cube([SAD_BORE_D * 2, SAD_BORE_D, SAD_T + 2*EPS]); } } // Slot for tray wall (negative) translate([0, SAD_T, -EPS]) cube([SAD_W, arm_gap, arm_len + EPS]); // M3 tie-down hole translate([SAD_W/2, SAD_T + arm_gap/2, -EPS]) cylinder(d=M3_D, h=SAD_H + 2*EPS); } } // ── Assembly ──────────────────────────────────────────────── module assembly() { // Tray body (open face up for visibility) color("SteelBlue") tray_body(); // Two T-nut brackets underneath at 1/4 and 3/4 length for (bx = [TRAY_L/4 - BKT_L/2, 3*TRAY_L/4 - BKT_L/2]) color("DodgerBlue") translate([bx, 0, -BKT_T]) tnut_bracket(); // Channel clips (3 per divider position, every 80 mm) for (i = [0:2]) { cx = 40 + i * 80; // Divider A/B color("Tomato", 0.8) translate([cx - 4, TRAY_WALL + ZONE_A_W - 2, TRAY_DEPTH - 3]) channel_clip(); // Divider B/C color("Orange", 0.8) translate([cx - 4, TRAY_WALL + ZONE_A_W + DIV_T + ZONE_B_W - 2, TRAY_DEPTH - 3]) channel_clip(); } // Cover panel (raised above tray to show interior) color("LightSteelBlue", 0.5) translate([0, 0, TRAY_DEPTH + SNAP_LEDGE_H + 4]) cover_panel(); // Cable saddles along front tray edge for (x = [40, 120, 200]) color("SlateGray") translate([x - SAD_W/2, -SAD_T * 2 - TRAY_WALL, 0]) cable_saddle(); } // ── Dispatch ──────────────────────────────────────────────── if (RENDER == "tray_body") tray_body(); else if (RENDER == "tnut_bracket") tnut_bracket(); else if (RENDER == "channel_clip") channel_clip(); else if (RENDER == "cover_panel") cover_panel(); else if (RENDER == "cable_saddle") cable_saddle(); else assembly();