// ============================================================
// ip54_enclosure.scad — IP54 Main Electronics Enclosure
// Issue: #144  Agent: sl-mechanical  Date: 2026-03-01
// ============================================================
//
// Sealed electronics bay for Jetson Orin NX + FC + ESC stack.
// IP54 rating: dust-protected, splash-proof from all directions.
//
// Protection method:
//   • 4 mm PETG walls (5 perims, 40 % infill)
//   • 2 mm silicone O-ring (Ø2 mm cord) in lid groove → IP54 seal
//   • PG7 cable glands (Ø3–6 mm cables) on rear wall × 4
//   • PG9 cable glands (Ø4–8 mm cables) on rear wall × 2
//   • 40 mm axial fan + foam filter panel on lid (positive pressure)
//   • Thermal: 2× Al heat sink pads on lid underside over Jetson/ESC
//
// Internal envelope: 220 × 160 × 90 mm (W × D × H, internal)
//   Fits: Jetson Orin NX (58 × 49 mm), FC 30.5 × 30.5 mm,
//         dual ESC (~80 × 40 mm each)
//
// Quick-release lid: 4× spring-loaded quarter-turn latches.
//   Tool-free. Lid lifts straight up after 90° rotation of each latch.
//
// Coordinates: Z = 0 at box floor (internal), Z+ upward.
//   Box centred on X=0, Y=0.
//
// RENDER options:
//   "assembly"     full box + lid + fans + glands preview
//   "body_stl"     box body (print 1×)
//   "lid_stl"      lid with fan mount (print 1×)
//   "fan_duct_stl" filtered fan inlet duct (print 1×)
//   "latch_stl"    quarter-turn latch knob (print 4×)
//   "gasket_2d"    DXF — lid O-ring groove outline + cable gland panel
//
// Export:
//   openscad ip54_enclosure.scad -D 'RENDER="body_stl"'     -o ip54_body.stl
//   openscad ip54_enclosure.scad -D 'RENDER="lid_stl"'      -o ip54_lid.stl
//   openscad ip54_enclosure.scad -D 'RENDER="fan_duct_stl"' -o ip54_fan_duct.stl
//   openscad ip54_enclosure.scad -D 'RENDER="latch_stl"'    -o ip54_latch.stl
//   openscad ip54_enclosure.scad -D 'RENDER="gasket_2d"'    -o ip54_gasket.dxf
// ============================================================

$fn = 64;
e   = 0.01;

// ── Internal cavity ───────────────────────────────────────────────────────────
INT_W   = 220.0;   // internal width  (X)
INT_D   = 160.0;   // internal depth  (Y)
INT_H   =  90.0;   // internal height (Z)

// ── Wall / structural ─────────────────────────────────────────────────────────
WALL    =   4.0;   // box wall thickness
LID_T   =   5.0;   // lid thickness
BOX_R   =   8.0;   // outer corner radius (XY)

// Derived outer dims
OUT_W   = INT_W + 2*WALL;
OUT_D   = INT_D + 2*WALL;
OUT_H   = INT_H + WALL;    // wall on floor + sides; lid closes top

// ── O-ring seal ───────────────────────────────────────────────────────────────
// 2 mm cord silicone O-ring in a groove on the lid flange inner face.
// Groove: 2.2 mm wide × 1.7 mm deep (standard 70 % compression for IP54).
ORING_D        = 2.0;
ORING_GROOVE_W = 2.2;
ORING_GROOVE_D = 1.7;
ORING_INSET    = 6.0;   // groove CL from inner box wall edge

// ── Lid flange (overlap joint) ────────────────────────────────────────────────
// Lid has a stepped rim that overlaps the box top edge.
// Seal groove is cut into the underside of this rim.
FLANGE_T    = 3.0;    // vertical flange depth (how far rim drops into box)
FLANGE_WALL = 3.0;    // rim wall thickness

// ── Quarter-turn latches ──────────────────────────────────────────────────────
// 4× positions: one per side (front/rear/left/right centre).
// Spring-loaded bayonet latch post on box side; rotating knob on lid flange.
LATCH_POST_D  = 10.0;   // latch post OD
LATCH_BOSS_H  =  8.0;   // boss height above box top flange
LATCH_KNOB_D  = 18.0;   // knob OD
LATCH_SLOT_W  =  2.5;   // bayonet slot width

// ── Cable glands ──────────────────────────────────────────────────────────────
// All glands on rear wall (Y = -OUT_D/2).
// PG7 thread OD = 12.5 mm; PG9 thread OD = 15.2 mm.
PG7_BORE  = 12.7;    // drill diameter for PG7 panel hole
PG9_BORE  = 15.4;    // drill diameter for PG9 panel hole
PG7_COUNT = 4;
PG9_COUNT = 2;

// Gland layout on rear wall (Y = -OUT_D/2 face), evenly spaced in X.
// Z centre = 30 mm from floor (lower half of box, cable routing stays low).
GLAND_Z     = 30.0;

// ── Fan ───────────────────────────────────────────────────────────────────────
// 40 mm axial fan in lid, front-left quadrant.
// Positive pressure: fan blows IN, filtered foam panel on top.
// Exit: passive vent slots on rear lid (over gland panel), IP54 labyrinth.
FAN_SZ      = 40.0;    // 40 mm fan
FAN_BORE_D  = 36.5;    // airflow bore
FAN_BOLT_SPC= 32.0;    // M3 bolt square (32 × 32 mm)
FAN_BOLT_D  =  3.3;
FAN_POS_X   = -INT_W/2 + FAN_SZ/2 + 10;  // fan X offset from centre (front-left)
FAN_POS_Y   = -INT_D/2 + FAN_SZ/2 + 10;  // fan Y offset

// ── Fan filter duct ───────────────────────────────────────────────────────────
DUCT_H      = 20.0;   // filter duct height above lid
FOAM_T      =  5.0;   // foam filter thickness

// ── Exhaust labyrinth slots ───────────────────────────────────────────────────
// Baffle-protected exhaust on lid rear.  2-row labyrinth prevents direct
// water ingress while maintaining IP54 (deflects splash from all angles).
EXH_SLOT_W  =  3.0;   // slot width
EXH_SLOT_L  = 30.0;   // slot length
EXH_ROWS    =  2;     // number of baffle rows
EXH_BAFFLE_H= 8.0;    // baffle height above lid
EXH_N_SLOTS = 4;      // number of exhaust slots per row

// ── Internal standoffs ────────────────────────────────────────────────────────
// Jetson Orin NX: 58 × 49 mm M3 hole pattern (4 holes), Z = WALL from floor
ORIN_HOLE_X = 58.0 / 2;
ORIN_HOLE_Y = 49.0 / 2;
ORIN_STOFF_H= 8.0;    // standoff height (PCB float)
ORIN_POS_X  = +INT_W/4;  // offset from box centre: right half
ORIN_POS_Y  =  0;

// FC: 30.5 × 30.5 mm M3 pattern
FC_HOLE_SPC = 30.5 / 2;
FC_STOFF_H  =  6.0;
FC_POS_X    = -INT_W/4;  // left half
FC_POS_Y    = -INT_D/4;

// ESC pair: 2× ESC ~80 × 40 mm; 4× M3 holes at corners
ESC_W       = 80.0;
ESC_D       = 40.0;
ESC_STOFF_H =  6.0;
ESC_POS_X   = -INT_W/4;
ESC_POS_Y   = +INT_D/4;

// ── Heat sink pads ────────────────────────────────────────────────────────────
// Recesses in lid underside that accept adhesive Al heat sink pads.
// Thermal path: board → heat sink → lid → ambient convection + fan.
// Pad size: 60 × 40 × 2 mm for Jetson, 50 × 30 × 2 mm for ESC.
HSINK_JETSON_W = 60.0;
HSINK_JETSON_D = 40.0;
HSINK_ESC_W    = 50.0;
HSINK_ESC_D    = 30.0;
HSINK_T        =  2.2;   // recess depth (pad sits flush in lid)

// Fasteners
M3_D = 3.3;
M4_D = 4.3;
M5_D = 5.3;

// ============================================================
// RENDER DISPATCH
// ============================================================
RENDER = "assembly";

if (RENDER == "assembly") {
    assembly();
} else if (RENDER == "body_stl") {
    box_body();
} else if (RENDER == "lid_stl") {
    box_lid();
} else if (RENDER == "fan_duct_stl") {
    fan_filter_duct();
} else if (RENDER == "latch_stl") {
    latch_knob();
} else if (RENDER == "gasket_2d") {
    projection(cut = true)
        translate([0, 0, -0.5])
            linear_extrude(1)
                gasket_profile_2d();
}

// ============================================================
// ASSEMBLY PREVIEW
// ============================================================
module assembly() {
    // Box body
    color("DarkOliveGreen", 0.85) box_body();

    // Lid (lifted 5 mm to show interior)
    color("OliveDrab", 0.70)
        translate([0, 0, OUT_H + 5])
            box_lid();

    // Fan duct on lid
    color("SaddleBrown", 0.80)
        translate([FAN_POS_X, FAN_POS_Y, OUT_H + LID_T + 5])
            fan_filter_duct();

    // Ghost latch knobs
    for (lpos = latch_positions())
        %color("DimGray", 0.60)
            translate([lpos[0], lpos[1], OUT_H + 3])
                latch_knob();

    // Ghost Jetson PCB
    %color("Green", 0.3)
        translate([ORIN_POS_X, ORIN_POS_Y, WALL + ORIN_STOFF_H])
            cube([58, 49, 3], center = true);

    // Ghost FC
    %color("Orange", 0.3)
        translate([FC_POS_X, FC_POS_Y, WALL + FC_STOFF_H])
            cube([30.5, 30.5, 3], center = true);

    // Ghost ESC
    %color("Red", 0.3)
        translate([ESC_POS_X, ESC_POS_Y, WALL + ESC_STOFF_H])
            cube([ESC_W, ESC_D, 3], center = true);

    // Index annotations (cable gland markers)
    for (gpos = cable_gland_positions())
        %color("Yellow", 0.5)
            translate([gpos[0], -OUT_D/2 - 5, gpos[1]])
                rotate([90, 0, 0])
                    cylinder(d = gpos[2], h = 3);
}

// ============================================================
// BOX BODY
// ============================================================
module box_body() {
    difference() {
        union() {
            // ── Outer shell (rounded rect, open top) ────────────────────
            _rounded_box(OUT_W, OUT_D, OUT_H, BOX_R);

            // ── Latch posts on top flange ────────────────────────────────
            for (lpos = latch_positions())
                translate([lpos[0], lpos[1], OUT_H])
                    _latch_post();

            // ── Cable gland boss pads (rear wall reinforcement) ──────────
            translate([0, -OUT_D/2, GLAND_Z])
                rotate([90, 0, 0])
                    _gland_boss_array();
        }

        // ── Internal cavity ──────────────────────────────────────────────
        translate([0, 0, WALL])
            cube([INT_W, INT_D, INT_H + e], center = true);

        // ── Cable gland holes (rear wall) ────────────────────────────────
        for (gpos = cable_gland_positions())
            translate([gpos[0], -OUT_D/2 - e, GLAND_Z])
                rotate([90, 0, 0])
                    cylinder(d = gpos[2], h = WALL + 2*e);

        // ── Internal standoffs (subtracted from floor thickness) ─────────
        // These are ADDED in the union; we only subtract if needed for wires.
    }

    // ── Internal PCB standoffs (added in separate union) ─────────────────
    // Jetson Orin NX standoffs
    for (sx = [-1, 1]) for (sy = [-1, 1])
        translate([ORIN_POS_X + sx*ORIN_HOLE_X,
                   ORIN_POS_Y + sy*ORIN_HOLE_Y,
                   WALL])
            difference() {
                cylinder(d = 7, h = ORIN_STOFF_H);
                cylinder(d = M3_D, h = ORIN_STOFF_H + e);
            }

    // FC standoffs
    for (sx = [-1, 1]) for (sy = [-1, 1])
        translate([FC_POS_X + sx*FC_HOLE_SPC,
                   FC_POS_Y + sy*FC_HOLE_SPC,
                   WALL])
            difference() {
                cylinder(d = 7, h = FC_STOFF_H);
                cylinder(d = M3_D, h = FC_STOFF_H + e);
            }

    // ESC standoffs (4 corners of ESC_W × ESC_D)
    for (sx = [-1, 1]) for (sy = [-1, 1])
        translate([ESC_POS_X + sx*(ESC_W/2 - 5),
                   ESC_POS_Y + sy*(ESC_D/2 - 5),
                   WALL])
            difference() {
                cylinder(d = 7, h = ESC_STOFF_H);
                cylinder(d = M3_D, h = ESC_STOFF_H + e);
            }
}

// ── Rounded box shell (open top) ─────────────────────────────────────────────
module _rounded_box(w, d, h, r) {
    linear_extrude(h)
        minkowski() {
            square([w - 2*r, d - 2*r], center = true);
            circle(r = r);
        }
}

// ── Latch post (on top rim of box) ───────────────────────────────────────────
module _latch_post() {
    difference() {
        cylinder(d = LATCH_POST_D + 4, h = LATCH_BOSS_H);
        // Central latch bore
        translate([0, 0, -e])
            cylinder(d = LATCH_POST_D, h = LATCH_BOSS_H + 2*e);
        // Bayonet slot (cross-slot in post top, for knob lug engagement)
        for (a = [0, 90])
            rotate([0, 0, a])
                translate([0, 0, LATCH_BOSS_H/2])
                    cube([LATCH_SLOT_W, LATCH_POST_D + 2, LATCH_BOSS_H],
                         center = true);
    }
}

// ── Cable gland boss array (pad behind gland holes on outer wall) ─────────────
module _gland_boss_array() {
    for (gpos = cable_gland_positions())
        translate([gpos[0], 0, 0])
            cylinder(d = gpos[2] + 8, h = 2);
}

// ── Latch positions (4 sides, centred) ───────────────────────────────────────
function latch_positions() = [
    [ 0,            +OUT_D/2 - 3 ],   // front
    [ 0,            -OUT_D/2 + 3 ],   // rear
    [ +OUT_W/2 - 3,  0           ],   // right
    [ -OUT_W/2 + 3,  0           ],   // left
];

// ── Cable gland positions [x, z, bore_d] on rear wall ────────────────────────
// 4× PG7 + 2× PG9, arranged in a row at GLAND_Z height.
// PG7 for signal / small power; PG9 for main drive harness.
function cable_gland_positions() = [
    [ -INT_W/2 + 15,  GLAND_Z,  PG7_BORE ],  // PG7 #1
    [ -INT_W/2 + 40,  GLAND_Z,  PG7_BORE ],  // PG7 #2
    [ -INT_W/2 + 65,  GLAND_Z,  PG7_BORE ],  // PG7 #3
    [ -INT_W/2 + 90,  GLAND_Z,  PG7_BORE ],  // PG7 #4
    [  INT_W/2 - 30,  GLAND_Z,  PG9_BORE ],  // PG9 #1 (main battery harness)
    [  INT_W/2 - 60,  GLAND_Z,  PG9_BORE ],  // PG9 #2 (motor harness)
];

// ============================================================
// BOX LID
// ============================================================
module box_lid() {
    difference() {
        union() {
            // ── Top plate ───────────────────────────────────────────────
            _rounded_box(OUT_W, OUT_D, LID_T, BOX_R);

            // ── Flanged rim (overlaps box top; O-ring groove cut into it) ─
            difference() {
                translate([0, 0, -FLANGE_T])
                    _rounded_box(INT_W + 2*FLANGE_WALL,
                                 INT_D + 2*FLANGE_WALL,
                                 FLANGE_T + e, BOX_R - 1);

                // Hollow interior of flange (sits over box rim)
                translate([0, 0, -FLANGE_T - e])
                    cube([INT_W, INT_D, FLANGE_T + 2*e], center = true);
            }

            // ── Exhaust labyrinth baffles on rear of lid ─────────────────
            _exhaust_baffles();
        }

        // ── Fan bore (through lid) ────────────────────────────────────────
        translate([FAN_POS_X, FAN_POS_Y, -e])
            cylinder(d = FAN_BORE_D, h = LID_T + 2*e);

        // ── Fan bolt holes ────────────────────────────────────────────────
        for (fx = [-1, 1]) for (fy = [-1, 1])
            translate([FAN_POS_X + fx*FAN_BOLT_SPC/2,
                       FAN_POS_Y + fy*FAN_BOLT_SPC/2, -e])
                cylinder(d = FAN_BOLT_D, h = LID_T + 2*e);

        // ── O-ring groove in flange underside ─────────────────────────────
        // Groove runs along the inner perimeter of the flange.
        translate([0, 0, -ORING_GROOVE_D])
            difference() {
                _rounded_box(
                    INT_W + 2*FLANGE_WALL - 2*(FLANGE_WALL - ORING_INSET),
                    INT_D + 2*FLANGE_WALL - 2*(FLANGE_WALL - ORING_INSET),
                    ORING_GROOVE_D + e, BOX_R - 2);
                _rounded_box(
                    INT_W + 2*FLANGE_WALL - 2*(FLANGE_WALL - ORING_INSET) - 2*ORING_GROOVE_W,
                    INT_D + 2*FLANGE_WALL - 2*(FLANGE_WALL - ORING_INSET) - 2*ORING_GROOVE_W,
                    ORING_GROOVE_D + 2*e, BOX_R - 3);
            }

        // ── Latch knob counterbores in lid flange (knob sits flush) ──────
        for (lpos = latch_positions())
            translate([lpos[0], lpos[1], -FLANGE_T - e])
                cylinder(d = LATCH_KNOB_D + 1, h = FLANGE_T + 2*e);

        // ── Heat sink pad recesses (underside of lid) ─────────────────────
        // Jetson pad recess
        translate([ORIN_POS_X, ORIN_POS_Y, -e])
            cube([HSINK_JETSON_W, HSINK_JETSON_D, HSINK_T + e], center = true);
        // ESC pad recess
        translate([ESC_POS_X, ESC_POS_Y, -e])
            cube([HSINK_ESC_W, HSINK_ESC_D, HSINK_T + e], center = true);

        // ── Exhaust labyrinth slot through-holes ──────────────────────────
        for (slot = exhaust_slot_positions())
            translate([slot[0], slot[1], -e])
                cube([EXH_SLOT_W, EXH_SLOT_L, LID_T + 2*e], center = true);
    }

    // ── Latch knob receiver rings in flange ─────────────────────────────────
    for (lpos = latch_positions())
        translate([lpos[0], lpos[1], -FLANGE_T])
            difference() {
                cylinder(d = LATCH_KNOB_D, h = FLANGE_T - 0.5);
                // Bayonet lugs engage latch post slot
                translate([0, 0, FLANGE_T - 0.5 - 3])
                    cylinder(d = LATCH_POST_D + 0.4, h = 3 + e);
                cylinder(d = LATCH_POST_D - 3, h = FLANGE_T + e);
            }
}

// ── Exhaust baffle array ──────────────────────────────────────────────────────
// Raised wall baffles on lid rear-right quadrant provide labyrinth exhaust path.
module _exhaust_baffles() {
    exh_x = INT_W/4;
    exh_y = INT_D/2 - 40;

    for (row = [0 : EXH_ROWS - 1])
        translate([exh_x, exh_y - row * (EXH_SLOT_W + 4), LID_T])
            cube([EXH_N_SLOTS * (EXH_SLOT_W + 6), EXH_BAFFLE_H/3, EXH_BAFFLE_H]);
}

// ── Exhaust slot positions [x, y] (in lid top surface) ───────────────────────
function exhaust_slot_positions() = [
    let(base_x = INT_W/4, base_y = INT_D/2 - 40)
    for (i = [0 : EXH_N_SLOTS - 1])
        [base_x - (EXH_N_SLOTS - 1)/2 * (EXH_SLOT_W + 6) + i * (EXH_SLOT_W + 6),
         base_y - EXH_SLOT_L/2]
];

// ============================================================
// FAN FILTER DUCT  (Part C — print 1×)
// ============================================================
// Sits on top of lid fan bore.  Contains 5 mm foam filter pad.
// Labyrinth inlet around sides prevents direct splash ingress.
module fan_filter_duct() {
    duct_od_w = FAN_SZ + 2*WALL;
    foam_slot = FOAM_T + 0.5;   // foam insert slot depth

    difference() {
        // Outer duct body
        cube([duct_od_w, duct_od_w, DUCT_H], center = true);

        // Foam filter slot (open at top for insert/remove)
        translate([0, 0, DUCT_H/2 - foam_slot - 1])
            cube([FAN_SZ, FAN_SZ, foam_slot + 2*e], center = true);

        // Airflow bore below foam (connects to fan bore in lid)
        translate([0, 0, -DUCT_H/2 - e])
            cylinder(d = FAN_BORE_D, h = DUCT_H/2 + 2*e);

        // Inlet slots on all 4 sides (labyrinth — no direct top-spray path)
        for (a = [0, 90, 180, 270])
            rotate([0, 0, a])
                translate([0, duct_od_w/2 - WALL/2, 0]) {
                    // Three inlet slots, staggered vertically
                    for (sz = [-DUCT_H/2 + 5, 0, DUCT_H/2 - 8])
                        translate([0, 0, sz])
                            cube([FAN_SZ * 0.5, WALL + 2*e, 5], center = true);
                }

        // Fan bolt holes (align to lid bolt holes)
        for (fx = [-1, 1]) for (fy = [-1, 1])
            translate([fx*FAN_BOLT_SPC/2, fy*FAN_BOLT_SPC/2,
                       -DUCT_H/2 - e])
                cylinder(d = FAN_BOLT_D, h = DUCT_H + 2*e);
    }
}

// ============================================================
// QUARTER-TURN LATCH KNOB  (Part D — print 4×)
// ============================================================
// Screws onto latch post from outside.  90° rotation latches/unlatches.
// Spring washer (not printed) provides axial preload.
module latch_knob() {
    knob_h = 12.0;
    grip_h =  8.0;

    difference() {
        union() {
            // Body disc
            cylinder(d = LATCH_KNOB_D, h = knob_h);
            // Grip ridges (6×, for finger purchase)
            for (i = [0:5])
                rotate([0, 0, i * 60])
                    translate([LATCH_KNOB_D/2 - 1, 0, 0])
                        cylinder(d = 2.5, h = grip_h);
        }

        // Latch post bore (clearance)
        translate([0, 0, -e])
            cylinder(d = LATCH_POST_D + 0.5, h = knob_h + 2*e);

        // Bayonet lug slot (catches latch post cross-slot)
        // 2 lugs at 180° — quarter-turn locks both simultaneously
        for (a = [0, 180])
            rotate([0, 0, a])
                translate([LATCH_POST_D/2 + LATCH_SLOT_W/2, 0, 5])
                    cube([LATCH_SLOT_W + 0.3, LATCH_POST_D, knob_h],
                         center = true);
    }
}

// ============================================================
// GASKET PROFILE 2D  (for DXF export)
// ============================================================
// Outputs the O-ring groove path as a 2D outline suitable for
// laser-cutting a flat silicone sheet gasket (alternative to O-ring cord).
// Sheet gasket: 2 mm silicone sheet, cut to this profile.
module gasket_profile_2d() {
    oring_cl_offset = FLANGE_WALL - ORING_INSET;
    outer_w = INT_W + 2*FLANGE_WALL - 2*oring_cl_offset;
    inner_w = outer_w - 2*ORING_GROOVE_W;
    r_outer = BOX_R - 2;
    r_inner = r_outer - ORING_GROOVE_W;

    difference() {
        minkowski() {
            square([outer_w - 2*r_outer, INT_D + 2*FLANGE_WALL - 2*oring_cl_offset - 2*r_outer],
                   center = true);
            circle(r = r_outer);
        }
        minkowski() {
            square([inner_w - 2*r_inner,
                    INT_D + 2*FLANGE_WALL - 2*oring_cl_offset - 2*r_inner - 2*ORING_GROOVE_W],
                   center = true);
            circle(r = r_inner);
        }
    }
}
