411 lines
17 KiB
OpenSCAD
411 lines
17 KiB
OpenSCAD
// ============================================================
|
||
// 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();
|