AnasSarkiz/BlinkBuddy

This code defines and renders a 24mm square PCB silkscreen with a centered QR code graphic and labeling for open project instructions.

Version
1.0.11
License
unset
Stars
0

enclosure/blinkbuddy_case.scad

// BlinkBuddy printable enclosure.
// Open in OpenSCAD, set part to "base", "top", "buttons", "sensor_windows", or "assembly", then export STL.

part = "assembly";

$fn = 64;

body_color = [0.02, 0.06, 0.20, 1.0];          // midnight blue
top_color = [0.04, 0.14, 0.36, 1.0];           // rich blue top
bezel_color = [0.0, 0.025, 0.08, 1.0];         // near-navy OLED surround
button_color = [1.0, 0.44, 0.08, 1.0];         // bright orange button caps
sensor_color = [0.60, 0.95, 1.0, 0.5];         // translucent ice blue preview
pcb_color = [0.18, 0.23, 0.26, 0.22];          // muted transparent PCB preview

// PCB dimensions from index.circuit.tsx
pcb_w = 82;
pcb_h = 60;
pcb_radius = 2;
pcb_thickness = 1.6;
pcb_clearance = 0.35;

// Case tuning
wall = 2.0;
bottom_thickness = 1.6;
base_wall_height = 16.0;
top_plate_thickness = 2.2;
top_air_gap = 16.5;
corner_radius = 4.0;
oled_clearance_margin = 1.8;
oled_bezel_height = 3.2;
pcb_support_height = 1.25;
mount_hole_d = 3.1;
mount_post_d = 2.6;
mount_post_clearance_to_cover = 0.3;
mount_post_height = top_air_gap - pcb_support_height - mount_post_clearance_to_cover;
mount_pedestal_d = 5.6;
m2_pilot_d = 1.45;
m2_clearance_d = 3.2;
screw_head_d = 6.0;
screw_head_depth = 1.4;
top_lip_depth = 4.0;
top_lip_wall = 1.2;
top_lip_clearance = 0.30;
snap_rib_thickness = 0.28;
snap_rib_length = 10.0;
snap_rib_height = 1.2;

outer_w = pcb_w + 2 * (wall + pcb_clearance);
outer_h = pcb_h + 2 * (wall + pcb_clearance);
inner_w = pcb_w + 2 * pcb_clearance;
inner_h = pcb_h + 2 * pcb_clearance;
lip_x_edge = inner_w / 2 - top_lip_clearance - top_lip_wall / 2;
lip_y_edge = inner_h / 2 - top_lip_clearance - top_lip_wall / 2;

// Board coordinate features. Same XY origin as PCB.
oled_x = 1;
oled_y = 2.5;
oled_body_w = 71.1;
oled_body_h = 42.1;
oled_window_w = 58;
oled_window_h = 35;

usb_x = 24;
usb_y = 30;
qwiic_x = -41;
qwiic_y = 4;

button_y = -23;
button_cutout = 8.2;
button_brow_height = 1.0;
switch_plunger_height = 5.0;
button_press_gap = 0.4;
button_rest_gap_above_brow = 0.35;
button_top_d = 9.6;
button_top_height = 1.6;
button_dome_height = 1.1;
button_guide_d = 7.0;
button_stem_d = 3.0;
button_positions = [
  [-18, button_y],
  [0, button_y],
  [18, button_y]
];
button_stem_depth = max(
  2.0,
  bottom_thickness + top_air_gap + top_plate_thickness + button_brow_height + button_rest_gap_above_brow
    - (bottom_thickness + pcb_support_height + pcb_thickness + switch_plunger_height + button_press_gap)
);

ldr_window_d = 9.5;
ldr_positions = [
  [-31.5, -26.4],
  [31.5, -26.2]
];

motion_window = [-37, -9];

// OLED mounting holes imported from HS242L03B2C01, shifted by OLED pcbX/Y.
oled_mount_holes = [
  [oled_x - 33.8755, oled_y + 19.5],
  [oled_x - 33.8755, oled_y - 19.5],
  [oled_x + 34.1244, oled_y - 19.5],
  [oled_x + 34.1244, oled_y + 19.5]
];

module rounded_rect_2d(w, h, r) {
  offset(r = r) {
    square([w - 2 * r, h - 2 * r], center = true);
  }
}

module rounded_box(w, h, z, r) {
  linear_extrude(height = z) {
    rounded_rect_2d(w, h, r);
  }
}

module slot_2d(w, h, r = 1.5) {
  rounded_rect_2d(w, h, min(r, min(w, h) / 2 - 0.05));
}

module oled_screw_post(x, y) {
  translate([x, y, bottom_thickness]) {
    difference() {
      union() {
        // Wide low pedestal supports the PCB around the OLED/module hole.
        cylinder(d = mount_pedestal_d, h = pcb_support_height);

        // Slim screw post passes through the 3.1 mm board/OLED holes.
        translate([0, 0, pcb_support_height - 0.01]) {
          cylinder(d = mount_post_d, h = mount_post_height);
        }
      }

      // M2 self-tapping pilot hole. The screw comes down from the top cover.
      translate([0, 0, -0.1]) {
        cylinder(d = m2_pilot_d, h = pcb_support_height + mount_post_height + 0.3);
      }
    }
  }
}

module lip_bar(x, y, w, h) {
  translate([x, y, -top_lip_depth / 2]) {
    cube([w, h, top_lip_depth], center = true);
  }
}

module snap_rib_x(x, y) {
  translate([x, y, -top_lip_depth + snap_rib_height]) {
    cube([snap_rib_thickness, snap_rib_length, snap_rib_height], center = true);
  }
}

module snap_rib_y(x, y) {
  translate([x, y, -top_lip_depth + snap_rib_height]) {
    cube([snap_rib_length, snap_rib_thickness, snap_rib_height], center = true);
  }
}

module top_retention_lip() {
  // Split inner lip: guides the cover into the base while leaving USB, Qwiic,
  // antenna, and sensor areas open.
  union() {
    lip_bar(-18, lip_y_edge, 42, top_lip_wall);
    lip_bar(0, -lip_y_edge, 28, top_lip_wall);

    lip_bar(-lip_x_edge, -18, top_lip_wall, 20);
    lip_bar(-lip_x_edge, 21, top_lip_wall, 14);
    lip_bar(lip_x_edge, -22, top_lip_wall, 12);
    lip_bar(lip_x_edge, 20, top_lip_wall, 16);

    // Tiny friction ribs make the cover snug. Sand them down if the fit is too tight.
    snap_rib_y(0, -lip_y_edge - top_lip_wall / 2 - snap_rib_thickness / 2);
    snap_rib_x(-lip_x_edge - top_lip_wall / 2 - snap_rib_thickness / 2, -18);
    snap_rib_x(lip_x_edge + top_lip_wall / 2 + snap_rib_thickness / 2, -22);
    snap_rib_x(lip_x_edge + top_lip_wall / 2 + snap_rib_thickness / 2, 20);
  }
}

module pcb_shadow(z = 0.3) {
  color(pcb_color) {
    translate([0, 0, bottom_thickness + pcb_support_height]) {
      rounded_box(pcb_w, pcb_h, z, pcb_radius);
    }
  }
}

module base_tray() {
  difference() {
    rounded_box(outer_w, outer_h, bottom_thickness + base_wall_height, corner_radius);

    translate([0, 0, bottom_thickness]) {
      rounded_box(inner_w, inner_h, base_wall_height + 0.2, pcb_radius + pcb_clearance);
    }

    // USB-C cable mouth on the top edge. Oversized for molded USB-C plugs.
    translate([usb_x, outer_h / 2 - wall / 2, bottom_thickness + 4.2]) {
      cube([18, wall + 1.2, 8.6], center = true);
    }

    // Qwiic cable mouth on the left edge.
    translate([-outer_w / 2 + wall / 2, qwiic_y, bottom_thickness + 4.0]) {
      cube([wall + 1.2, 16, 8.0], center = true);
    }

    // Keep ESP antenna edge open and plastic-light.
    translate([outer_w / 2 - wall / 2, -5, bottom_thickness + 5.5]) {
      cube([wall + 1.2, 26, 11.0], center = true);
    }
  }

  // Screw posts aligned to OLED/module holes. The slim posts fit through the 3.1 mm holes.
  for (p = oled_mount_holes) {
    oled_screw_post(p[0], p[1]);
  }
}

module top_faceplate(zpos = 0) {
  translate([0, 0, zpos]) {
    difference() {
      union() {
        rounded_box(outer_w, outer_h, top_plate_thickness, corner_radius);

        top_retention_lip();

        oled_bezel();

        // Friendly rounded brow around buttons.
        translate([0, button_y, top_plate_thickness - 0.01]) {
          linear_extrude(height = 1.0) {
            slot_2d(62, 16, 4);
          }
        }
      }

      // OLED visible area.
      translate([oled_x, oled_y, -0.2]) {
        linear_extrude(height = top_plate_thickness + oled_bezel_height + 1.2) {
          slot_2d(oled_window_w, oled_window_h, 1.8);
        }
      }

      // Full OLED body relief underneath the faceplate so the cover does not press on the module frame.
      translate([oled_x, oled_y, -0.3]) {
        linear_extrude(height = top_plate_thickness + 0.6) {
          slot_2d(oled_body_w + 1.0, oled_body_h + 1.0, 2.5);
        }
      }

      // M2 screw clearance holes and shallow counterbores.
      for (p = oled_mount_holes) {
        translate([p[0], p[1], -0.2]) {
          cylinder(d = m2_clearance_d, h = top_plate_thickness + oled_bezel_height + 1.2);
        }
        translate([p[0], p[1], top_plate_thickness + oled_bezel_height - screw_head_depth]) {
          cylinder(d = screw_head_d, h = screw_head_depth + 0.4);
        }
      }

      // Button openings.
      for (p = button_positions) {
        translate([p[0], p[1], -0.2]) {
          cylinder(d = button_cutout, h = top_plate_thickness + 3.4);
        }
      }

      // Light sensor windows.
      for (p = ldr_positions) {
        translate([p[0], p[1], -0.2]) {
          cylinder(d = ldr_window_d, h = top_plate_thickness + 3.4);
        }
      }

      // Motion sensor window on left side.
      translate([motion_window[0], motion_window[1], -0.2]) {
        linear_extrude(height = top_plate_thickness + 3.4) {
          slot_2d(10, 10, 2);
        }
      }

      // USB-C top access relief.
      translate([usb_x, outer_h / 2 - 3.0, -0.2]) {
        linear_extrude(height = top_plate_thickness + 3.4) {
          slot_2d(20, 12, 2);
        }
      }

      // Qwiic access relief.
      translate([-outer_w / 2 + 3.0, qwiic_y, -0.2]) {
        linear_extrude(height = top_plate_thickness + 3.4) {
          slot_2d(10, 19, 2);
        }
      }

      // ESP antenna area: keep cover open near antenna side.
      translate([outer_w / 2 - 6, -5, -0.2]) {
        linear_extrude(height = top_plate_thickness + 3.4) {
          slot_2d(14, 24, 2);
        }
      }
    }
  }
}

module oled_bezel() {
  translate([oled_x, oled_y, top_plate_thickness - 0.01]) {
    linear_extrude(height = oled_bezel_height) {
      difference() {
        slot_2d(oled_body_w + 2 * oled_clearance_margin, oled_body_h + 2 * oled_clearance_margin, 3.0);
        slot_2d(oled_window_w, oled_window_h, 1.8);
      }
    }
  }
}

module button_caps() {
  for (i = [0 : len(button_positions) - 1]) {
    p = button_positions[i];
    translate([p[0], p[1], 0]) {
      union() {
        // Finger pad is wider than the top opening, so the loose cap cannot fall through.
        cylinder(d = button_top_d, h = button_top_height);
        translate([0, 0, button_top_height]) {
          cylinder(d1 = button_top_d, d2 = button_top_d - 1.0, h = button_dome_height);
        }

        // Guide section slides through the cover hole and keeps the cap centered.
        translate([0, 0, -(top_plate_thickness + button_brow_height + button_rest_gap_above_brow + 0.2)]) {
          cylinder(d = button_guide_d, h = top_plate_thickness + button_brow_height + button_rest_gap_above_brow + 0.8);
        }

        // Long plunger reaches down to the real tactile switch button.
        translate([0, 0, -button_stem_depth]) {
          cylinder(d = button_stem_d, h = button_stem_depth + 0.15);
        }
      }
    }
  }
}

module sensor_windows() {
  for (p = ldr_positions) {
    translate([p[0], p[1], 0]) {
      cylinder(d = ldr_window_d - 0.6, h = 0.8);
    }
  }

  translate([motion_window[0], motion_window[1], 0]) {
    linear_extrude(height = 0.8) {
      slot_2d(9.2, 9.2, 2);
    }
  }
}

module colored_top_faceplate(zpos = 0) {
  color(top_color) {
    top_faceplate(zpos);
  }

  color(bezel_color) {
    translate([0, 0, zpos]) {
      oled_bezel();
    }
  }
}

module assembly_preview() {
  color(body_color) {
    base_tray();
  }
  pcb_shadow();
  colored_top_faceplate(bottom_thickness + top_air_gap);
  color(button_color) {
    translate([0, 0, bottom_thickness + top_air_gap + top_plate_thickness + button_brow_height + button_rest_gap_above_brow]) {
      button_caps();
    }
  }
  color(sensor_color) {
    translate([0, 0, bottom_thickness + top_air_gap + top_plate_thickness + 0.05]) {
      sensor_windows();
    }
  }
}

if (part == "base") {
  base_tray();
} else if (part == "top") {
  top_faceplate();
} else if (part == "buttons") {
  translate([0, 0, button_stem_depth]) {
    button_caps();
  }
} else if (part == "sensor_windows") {
  sensor_windows();
} else {
  assembly_preview();
}