TerslerThe little junk rig that could

Source files / shroud · raw

p100_front_duct.scad

// Dual Tesla P100 front intake duct for one 120 mm fan (Arctic S12038-4K)
// Board: ASUS Z10PA-U8, cards in PCIE6 + PCIE4, in a tower case.
//
// Stack along the cards' axis, from the case front:
//
//   case wall | wall plate | 120x38 fan | duct ............ | card tails
//   0           plate_t      + fan_t      + duct_len         = wall_to_tail
//
// - The wall plate screws onto the case's existing 92 mm fan holes (from
//   outside), and holds four M4 nuts in traps on its wall side.
// - M4 x 50 bolts go from inside the duct flange through the fan into those
//   nuts, so the fan + duct can be mounted after the plate is on the wall.
// - The duct ends at the card tails (seal the last mm with foam tape). Its
//   top wall stays below the power plugs, so the cables leave upwards.
//
// Print the duct card end DOWN (flange on top). The fingers and the gap
// blocker then grow from the bed, the taper stays steeper than 45°, and the
// flange corners have 45° chamfers. The plate prints flat, either side down.
//
// Coordinates: z = airflow axis, z = 0 is the duct flange face (fan outlet side).
//              x = across the cards (stacking direction), y = card height,
//              +y = the cards' top edge (away from the board, where the plugs are).

/* [Case] */
wall_to_tail = 178;   // inner case wall to the card tail faces (measured ~180)
x_off        = 0;     // card stack centre relative to the fan axis, across the cards
y_off        = 0;     // ... along the card height (+ = towards the cards' top edge)
case_hole_sp = 82.5;  // the case's 92 mm fan holes, centre to centre
case_dx      = 0;     // centre of those holes relative to the 120 mm fan axis
case_dy      = 0;
case_pilot_d = 3.4;   // pilot hole for the screws from outside (self-tapping into the plate)

/* [Cards] */
card_h      = 111.2;  // tail face: PCB edge to top edge
card_t      = 37.0;   // thickness of one card's cooler
slot_pitch  = 40.64;  // centre-to-centre of the two cards
n_cards     = 2;
clearance   = 0.6;    // play per side at the fingers
plug_zone   = 24;     // MEASURE: card top edge to the lowest point of the power plug (incl. latch)

/* [Duct] */
wall        = 2.0;
flange_t    = 4;
chamfer_h   = 25;     // 45° support under the flange corners
finger_len  = 12;     // how far the side fingers and the gap blocker reach along the cards
bottom_finger = true; // also a finger under the cards' bottom (PCB) edge; avoids a bridge when printing
loft_steps  = 32;
r_end       = 3;      // corner radius at the card end

/* [Fan] */
fan_size    = 120;
fan_t       = 38;
fan_hole_sp = 106;    // measured on the actual fan
fan_hole_d  = 4.6;
fan_open_d  = 116;
head_d      = 8.5;    // counterbore for the M4 bolt heads (cap or button head) in the chamfer

/* [Wall plate] */
plate_t     = 8;
nut_af      = 7.0;    // M4 nut across flats
nut_clear   = 0.3;
nut_h       = 3.4;    // trap depth on the wall side
boss_r      = 5.5;    // material around the case screw holes inside the opening

/* [Gap blocker] */
gap_block       = true;
gap_block_clear = 0.3;
nose_len        = 25;
nose_edge       = 0.6;

/* [Output] */
part = "both";        // "duct", "plate", "both" (side by side), "assembly" (as installed)
cutaway = false;

$fn = 96;
e = 0.01;

// ---- derived ----
duct_len = wall_to_tail - plate_t - fan_t;   // flange face to card tails
stack_x  = slot_pitch * (n_cards - 1) + card_t;
in_x     = stack_x + 2 * clearance;
// mouth: from the cards' bottom edge up to just below the plugs
m_bot    = -card_h/2 - clearance;
m_top    =  card_h/2 - plug_zone - wall - 0.5;
in_y     = m_top - m_bot;
m_cx     = x_off;
m_cy     = y_off + (m_top + m_bot) / 2;
z_t0     = flange_t;
z_t1     = duct_len;
gap      = slot_pitch - card_t;
bt       = gap - 2 * gap_block_clear;

function lerp(a, b, s) = a + (b - a) * s;
function ease(t) = t * t * (3 - 2 * t);

module section(s, grow = 0) {
    w = lerp(fan_open_d, in_x, s);
    h = lerp(fan_open_d, in_y, s);
    r = lerp(fan_open_d / 2, r_end, s);
    translate([lerp(0, m_cx, s), lerp(0, m_cy, s)])
        hull() for (sx = [-1, 1], sy = [-1, 1])
            translate([sx * max(w/2 - r, 0), sy * max(h/2 - r, 0)]) circle(r = r + grow);
}

module slice(t, grow = 0) {
    translate([0, 0, lerp(z_t0, z_t1, t) - e/2]) linear_extrude(e) section(ease(t), grow);
}

module loft(grow = 0) {
    for (i = [0 : loft_steps - 1])
        hull() { slice(i / loft_steps, grow); slice((i + 1) / loft_steps, grow); }
}

module duct_outer() {
    // flange with 45° chamfered corners underneath
    hull() {
        translate([-fan_size/2, -fan_size/2, 0]) cube([fan_size, fan_size, flange_t]);
        translate([0, 0, flange_t + chamfer_h]) cylinder(d = fan_open_d + 2*wall, h = e);
    }
    loft(wall);
}

module duct_air() {
    translate([0, 0, -e]) cylinder(d = fan_open_d, h = flange_t + 2*e);
    loft(0);
    // straight continuation past the tails, so the fingers stay open inside
    translate([m_cx - in_x/2, m_cy - in_y/2, z_t1 - e]) cube([in_x, in_y, finger_len + 1]);
}

module fingers() {
    for (sx = [-1, 1])   // side walls along the outer faces of the stack
        translate([m_cx + sx * (in_x/2 + wall/2) - wall/2, m_cy - in_y/2 - wall, z_t1 - e])
            cube([wall, in_y + wall, finger_len + e]);
    if (bottom_finger)   // under the cards' bottom edge
        translate([m_cx - in_x/2 - wall, m_cy - in_y/2 - wall, z_t1 - e])
            cube([in_x + 2*wall, wall, finger_len + e]);
}

function gap_x(i) = x_off - stack_x/2 + i * slot_pitch - gap/2;

module gap_blockers() {
    if (bt > 0.8)
        for (i = [1 : n_cards - 1]) {
            xc = gap_x(i);
            translate([xc - bt/2, m_cy - in_y/2 - e, z_t1 - e]) cube([bt, in_y + 2*e, finger_len + e]);
            intersection() {
                loft(0);
                hull() {
                    translate([xc - bt/2, -fan_size, z_t1 - e]) cube([bt, 2*fan_size, e]);
                    translate([xc - nose_edge/2, -fan_size, z_t1 - nose_len]) cube([nose_edge, 2*fan_size, e]);
                }
            }
        }
}

module fan_bolt_holes() {
    for (sx = [-1, 1], sy = [-1, 1])
        translate([sx * fan_hole_sp/2, sy * fan_hole_sp/2, 0]) {
            translate([0, 0, -e]) cylinder(d = fan_hole_d, h = flange_t + 2*e);
            translate([0, 0, flange_t]) cylinder(d = head_d, h = chamfer_h + 1);   // head access
        }
}

module duct() {
    difference() {
        duct_outer();
        duct_air();
        fan_bolt_holes();
    }
    fingers();
    if (gap_block && n_cards > 1) gap_blockers();
}

// plate: z = 0 is the wall side
module plate() {
    difference() {
        translate([-fan_size/2, -fan_size/2, 0]) cube([fan_size, fan_size, plate_t]);
        // opening, minus small bosses around the case screws
        difference() {
            translate([0, 0, -e]) cylinder(d = fan_open_d, h = plate_t + 2*e);
            for (sx = [-1, 1], sy = [-1, 1])
                translate([case_dx + sx * case_hole_sp/2, case_dy + sy * case_hole_sp/2, -1])
                    cylinder(r = boss_r, h = plate_t + 2);
        }
        for (sx = [-1, 1], sy = [-1, 1]) {
            // case screws, from outside
            translate([case_dx + sx * case_hole_sp/2, case_dy + sy * case_hole_sp/2, -e])
                cylinder(d = case_pilot_d, h = plate_t + 2*e);
            // fan bolts + nut traps on the wall side
            translate([sx * fan_hole_sp/2, sy * fan_hole_sp/2, -e]) {
                cylinder(d = fan_hole_d, h = plate_t + 2*e);
                cylinder(d = (nut_af + 2*nut_clear) / cos(30), h = nut_h + e, $fn = 6);
            }
        }
    }
}

module cut() { translate([0, -150, -50]) cube([300, 300, 400]); }

if (part == "assembly") {
    color("silver") translate([0, 0, -plate_t - fan_t]) plate();
    color("dimgray", 0.5) translate([-fan_size/2, -fan_size/2, -fan_t]) cube([fan_size, fan_size, fan_t]);
    color("orange") duct();
    color("green", 0.4) for (i = [0 : n_cards - 1])   // card tails
        translate([x_off - stack_x/2 + i * slot_pitch, y_off - card_h/2, duct_len]) cube([card_t, card_h, 60]);
} else {
    if (part == "duct" || part == "both")
        if (cutaway) difference() { duct(); cut(); } else duct();
    if (part == "plate" || part == "both")
        translate([part == "both" ? fan_size + 15 : 0, 0, 0]) plate();
}

echo(str("duct length (flange face to tails): ", duct_len, " mm; mouth ", in_x, " x ", in_y,
         " mm; centre offset ", m_cx, " / ", m_cy, " mm"));
echo(str("bolts: M4 x 50, grip to the nut ", flange_t + fan_t + plate_t - nut_h, " mm"));