PwnFlakes

Untitled

Sep 16th, 2026
23,814
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.34 KB | None | 0 0
  1. // m5stackfp enclosure — AtomS3R + Unit Fingerprint2 in one slab.
  2. //
  3. // A FRAME, not a box. The short ends are OPEN: no bar of plastic across them,
  4. // only the rim turning the corner. So the AtomS3R's USB-C plugs straight in
  5. // with nothing above it, and at the far end the sensor's Grove cable leaves
  6. // through the void and drops away. Both devices sit hard against their end,
  7. // working faces flush with the top.
  8. //
  9. // The rim is ONE uniform width everywhere — down the sides, around the window
  10. // corners, and where it turns onto the end faces. At each end the rim turns the
  11. // corner and runs `arm` mm along the end face before the opening starts.
  12. //
  13. // Each device gets its OWN rounded pocket, divided by a 2 mm wall. A single
  14. // shared cavity has no wall where the devices meet, so there is nothing there to
  15. // round. Below the pockets the opening runs through, so the base plate passes
  16. // under the divider as one piece.
  17. //
  18. // No screws. The base plate press-fits into the bottom of the cavity, flush with
  19. // the shell's underside, and the devices sit on it. Screw bosses had to live in
  20. // the divider, which forced it to 7 mm — far too fat next to the original.
  21. //
  22. // Device sizes are exact, from M5Stack's own structure files:
  23. //   AtomS3R       24.00 x 24.00 x 12.90
  24. //   Fingerprint2  24.00 x 40.00 x  8.00
  25. //
  26. // Printing: shell top-face-down — best finish on the face you look at, and the
  27. // windows need no support. The base carries the pad that lifts the sensor, so
  28. // it grows up off the bed rather than hanging at the end of a shell print.
  29. // Tuned for an Elegoo Centauri Carbon in PLA.
  30. //
  31. //   openscad -o shell.stl -D 'part="shell"' enclosure.scad
  32. //   openscad -o base.stl  -D 'part="base"'  enclosure.scad
  33.  
  34. part = "all";          // "shell" | "base" | "all" | "preview"
  35.  
  36. /* [Fit] */
  37. clear      = 0.25;     // per-side slack around each device in X/Y
  38. fit        = 0.15;     // press-fit slack for the base plate in the cavity
  39. wall       = 2.4;
  40. top        = 2.0;      // material above the devices; windows cut through it
  41. floor_t    = 2.0;      // base plate, which sits INSIDE the cavity bottom
  42. corner_r   = 4.0;      // outer corners
  43. // Corner radii measured off M5Stack's own structure files. The Fingerprint2 is
  44. // stadium-shaped: ~3 mm where its connector is, ~12 mm at the other end — and
  45. // 12 mm on a 24 mm-wide module is a full semicircle. The AtomS3R is just a
  46. // rounded square. Pockets add `clear` to these.
  47. atom_r     = 5.3;
  48. fp2_r_out  = 3.0;      // connector end, faces the open right end of the case
  49. fp2_r_in   = 12.0;     // semicircular end, faces the divider
  50. lip        = 1.2;      // top overlap retaining each device
  51.  
  52. /* [Devices] */
  53. atom       = [24.0, 24.0, 12.90];
  54. fp2        = [24.0, 40.0,  8.00];   // 24 across the case, 40 along it
  55.  
  56. /* [Layout] */
  57. // The gap between the devices is a CABLE CHAMBER, not just a divider: the Grove
  58. // ribbon joining the two needs somewhere to sit. It is deliberately wider than
  59. // the visible divider bar — the windows overhang into it from both sides, so
  60. // the top still reads as a thin `div_top` rib while the space below it is open
  61. // for the ribbon and its plug.
  62. gap        = 7.0;      // cable chamber, full depth
  63. div_top    = 2.0;      // visible divider bar on the top face
  64.  
  65. /* [Windows] */
  66. end_r      = 3.0;      // rounding where the end opening meets the window
  67. end_w      = 13.0;     // open width at each end; the rest becomes the corner
  68.                       // arms, which need real length to grip the devices.
  69.                       // Deriving this from the rim made the opening exactly as
  70.                       // wide as the window, leaving no corner at all.
  71.  
  72. /* [Derived] */
  73. rim = wall + lip;               // uniform rim width, sides and corner turns
  74. ax = clear;
  75. fx = clear + atom[0] + gap;
  76. inner = [atom[0] + gap + fp2[1] + 2*clear, atom[1] + 2*clear, atom[2] + floor_t];
  77. outer = [inner[0] + 2*wall, inner[1] + 2*wall, inner[2] + top];
  78. fp_rise = atom[2] - fp2[2];     // pad height so the sensor face sits flush
  79. over = (gap - div_top)/2;       // window overhang into the cable chamber
  80. // The pockets start fractionally BELOW the base-plate slice rather than exactly
  81. // on it. Cut faces meeting on a shared plane leave a scatter of non-manifold
  82. // edges; overlapping downward avoids that without changing how the plate seats,
  83. // since the plate still fills the slice from 0 to floor_t.
  84. ov = 0.5;
  85. // Corner arm: `end_w` sets its length, and its thickness is the distance from
  86. // the outer face to where the window starts, which is `rim` by construction.
  87. arm = (outer[1] - end_w)/2;
  88.  
  89. module rrect(sz, r) {
  90.    hull() for (x = [r, sz[0]-r], y = [r, sz[1]-r])
  91.        translate([x, y, 0]) cylinder(r=r, h=sz[2], $fn=48);
  92. }
  93.  
  94. module window(x, y, w, d, h, r) {
  95.    translate([x, y, 0]) hull()
  96.        for (i = [r, w-r], j = [r, d-r])
  97.            translate([i, j, 0]) cylinder(r=r, h=h, $fn=40);
  98. }
  99.  
  100. // Stadium: a different radius at each end along X. Used for the Fingerprint2,
  101. // whose two ends are genuinely different shapes.
  102. module stadium(x, y, w, d, h, r_lo, r_hi) {
  103.    translate([x, y, 0]) hull() {
  104.        for (j = [r_lo, d - r_lo]) translate([r_lo, j, 0]) cylinder(r=r_lo, h=h, $fn=56);
  105.        for (j = [r_hi, d - r_hi]) translate([w - r_hi, j, 0]) cylinder(r=r_hi, h=h, $fn=56);
  106.    }
  107. }
  108.  
  109. module shell() {
  110.    difference() {
  111.        rrect(outer, corner_r);
  112.  
  113.        // Bottom slice: one continuous opening for the base plate, which
  114.        // therefore passes under the divider as a single piece.
  115.        translate([wall, wall, -0.1])
  116.            rrect([inner[0], inner[1], floor_t + 0.1], atom_r);
  117.  
  118.        // Above that, a SEPARATE rounded pocket per device, so each is properly
  119.        // bounded — including where it meets the divider. A single shared
  120.        // cavity has no wall there to round.
  121.        translate([wall, wall, floor_t - ov])
  122.            rrect([atom[0] + 2*clear, inner[1], inner[2] - floor_t + 1 + ov],
  123.                  atom_r + clear);
  124.        stadium(wall + atom[0] + 2*clear + gap, wall,
  125.                fp2[1] + 2*clear, inner[1], inner[2] - floor_t + 1 + ov,
  126.                fp2_r_in + clear, fp2_r_out + clear);
  127.  
  128.        // Cable chamber between the pockets, full depth.
  129.        translate([wall + atom[0] + 2*clear, wall + 2, floor_t - ov])
  130.            cube([gap, inner[1] - 4, inner[2] - floor_t + 1 + ov]);
  131.  
  132.        // Top windows.
  133.        window(wall + ax + lip, wall + clear + lip,
  134.               atom[0] - lip + over, atom[1] - 2*lip, outer[2] + 1, atom_r - lip);
  135.        stadium(wall + fx + lip - over, wall + clear + lip,
  136.                fp2[1] - lip + over, fp2[0] - 2*lip, outer[2] + 1,
  137.                fp2_r_in - lip, fp2_r_out);
  138.  
  139.        // Open ends, full height, centred. What survives at each corner is an
  140.        // arm `arm` long whose thickness is the rim, so the rim reads as turning
  141.        // through 90 degrees onto the end face.
  142.        window(-2, arm, wall + 6, end_w, outer[2] + 1, end_r);
  143.        window(outer[0] - wall - 4, arm, wall + 6, end_w, outer[2] + 1, end_r);
  144.    }
  145. }
  146.  
  147. module base() {
  148.    // Press-fits into the bottom of the cavity, flush with the shell's
  149.     // underside. The devices then sit on it.
  150.     translate([wall + fit, wall + fit, 0]) {
  151.         rrect([inner[0] - 2*fit, inner[1] - 2*fit, floor_t], atom_r);
  152.         // Pad lifting the sensor so its face finishes flush with the top.
  153.         // Hollowed out: the ribbon needs to reach the sensor's connector, and
  154.        // the space under a raised sensor is the obvious place for it to run.
  155.        translate([fx - fit, clear - fit, floor_t])
  156.            difference() {
  157.                cube([fp2[1], fp2[0], fp_rise]);
  158.                translate([4, 4, -0.1]) cube([fp2[1] - 8, fp2[0] - 8, fp_rise + 0.2]);
  159.            }
  160.    }
  161. }
  162.  
  163. module devices() {   // preview only
  164.    color("#4477cc") translate([wall + ax, wall + clear, floor_t]) cube(atom);
  165.    color("#cc7744") translate([wall + fx, wall + clear, floor_t + fp_rise])
  166.        cube([fp2[1], fp2[0], fp2[2]]);
  167. }
  168.  
  169. if (part == "shell") shell();
  170. else if (part == "base") base();
  171. else if (part == "preview") { %shell(); devices(); }
  172. else { shell(); translate([0, outer[1] + 6, 0]) base(); }
  173.  
  174. echo(str("outer: ", outer[0], " x ", outer[1], " x ", outer[2], " mm"));
  175. echo(str("rim: ", rim, " mm everywhere; end opening ", end_w, " mm"));
  176. echo(str("divider: ", gap, " mm"));
  177.  
Advertisement