// ===================================================================== // Zn-MnO2 Cell Module Case (drawing set ZM-CELL-01, Rev D) // Parametric block for the salt-water battery: 1 to many cells, wired // in series, in parallel, or both. // Parts: box (item 1), lid (item 2), optional fit-test coupon. // Units: mm. Coordinates: X = 167.6 length (+ end at X=0), // Y = block thickness, Z = height. // ===================================================================== // // --------------------------------------------------------------------- // CELL LAYOUT (cells_series x cells_parallel) // --------------------------------------------------------------------- // cells_parallel (P): plate stacks that share ONE compartment and ONE // electrolyte. They are at the same voltage, so no partition is // needed. Each stack is the standard kit (5 cathodes, 6 zinc, felt), // placed side by side. All cathode tabs go to one Ti riser and all // zinc tabs to one brass riser. Electrolyte = P x 359 mL. // // cells_series (S): sealed compartments separated by solid partitions // printed with the box. NEVER drill or notch a partition: an // electrolyte path between compartments drains the cells. // Every compartment has its own vent, fill port and two lugs // through the lid. Alternate compartments are turned 180 degrees, // so the - lug of one cell sits beside the + lug of the next. // A raised link pad joins those two bosses. Bolt a short link bar // (3 mm Ni-brass) across them. The bars stay outside the sealed // space, and every cell remains reachable for a BMS sense lead. // // Block thickness is always S x P x 26.88 mm, so blocks drop into the // same slotted outer case as single modules. The guide rails repeat at // every slot. Any spare thickness goes into the outer side walls. // // Examples: // 1S1P single module (the original design) ............ 26.9 mm // 2S1P 2.6 V block, 5 per 12 V pack .................... 53.8 mm // 1S2P double-capacity cell ............................ 53.8 mm // 2S2P 2.6 V double-capacity block, 5 per 10S2P pack .. 107.5 mm // 5S1P 6.5 V block ..................................... 134.4 mm // Blocks wider than your bed's short side (about 220 mm) won't print // flat. A 10-cell monoblock (268.8 mm) needs a large-format printer. // // FILAMENT // --------------------------------------------------------------------- // Recommended: PETG // - Resists the mildly acidic 2 M ZnSO4 / 0.1 M MnSO4 electrolyte // (pH about 4-5) and absorbs very little water. // - Strong layer bonding, which is what keeps thin printed walls // watertight. // - Softens around 75-80 C, well above the 45 C BMS cutoff. // - Easy to print, and the lid can be bonded with epoxy. // // Best chemically, harder to print: Polypropylene (PP) // - The same material as the original design, and the lid can be // hot-plate welded. // - It warps badly and only sticks to a PP-tape or PP-sheet bed. // Use an enclosure. // - Epoxy does not stick to PP without flame or plasma treatment. // // Also acceptable: ASA (needs an enclosure because it warps). // // Do NOT use: // - PLA: slowly breaks down in warm acidic water, creeps under the // felt compression load, and softens near 55 C. // - Nylon / PA: absorbs water and swells. // - TPU: too flexible to hold the stack compression. // - Any carbon-fiber or metal-filled filament: it conducts, and // metal fillers react with the electrolyte. // // Buy natural (uncoloured) filament if you can. Pigments and fillers // are possible contaminants for the zinc. // // --------------------------------------------------------------------- // PRINT SETTINGS (PETG, 0.4 mm nozzle) // --------------------------------------------------------------------- // Layer height ........ 0.20 mm (0.16 for a tighter seal) // Line width .......... 0.40 mm, so the 1.2 mm walls are exactly // 3 perimeters. Check the slicer preview: the // walls must be solid perimeters with no gap fill. // Perimeters .......... 4 or more (the slicer caps them at the wall // thickness, and extra ones make ribs/bosses solid) // Top/bottom layers ... 6 / 6 // Infill .............. 100% (bosses, rails and ribs carry load) // Nozzle / bed ........ 240-250 C / 80 C // Cooling fan ......... 30-40% (better layer bonding = watertight) // Flow ................ +3-5% over calibrated, to close micro-gaps // Outer wall speed .... 25-30 mm/s // Orientation ......... Box: open top up, bottom on the bed. // Lid: as modelled (locating lip and baffle on // the bed). The lid plate bridges about 22 mm // across the lip, with no supports needed. // Supports ............ None // // After printing: // 1. Leak test the empty box: fill it with water and stand it on // paper towel for 24 hours. On a multi-cell block, also fill // ONE compartment at a time and check its neighbours stay dry. // 2. Optional extra seal: brush a thin coat of low-viscosity epoxy // on the inside of the box and let it cure fully. // 3. Tap the fill-port hole M16 x 1.5. Use a nylon or PP M16 x 1.5 // plug with an EPDM O-ring. // 4. Press in the M6 hex nuts (316 SS) before fitting the lugs. // Bolts: M6 x 12 on the + (Ti, 4 mm) lug and M6 x 10 on the // - (brass, 3 mm) lug, with a 3 mm link bar on top. On a // multi-cell block the inner link bars bolt across the link pads. // 5. Lid joint: PETG -> fill every lip gap with epoxy. On a // multi-cell block, check the epoxy closes over each // partition top: that bead is the seal between cells. // PP -> hot-plate or hot-air weld. // // --------------------------------------------------------------------- // SELECT WHAT TO RENDER // --------------------------------------------------------------------- part = "assembly"; // "box", "lid", "assembly", "exploded", "fit_test" // Export each STL with part = "box" and then part = "lid", e.g. // openscad -D part='"box"' -D cells_series=2 -D cells_parallel=2 \ // -o cell_case_2s2p_box.stl cell_module_case.scad cells_series = 1; // sealed compartments in series in this block cells_parallel = 1; // plate stacks sharing each compartment show_hardware = true; // lug pads, link bars and bolts in the // "assembly" and "exploded" views (not printed) $fn = 64; eps = 0.01; ns = cells_series; np = cells_parallel; // ----------------------------- Box ----------------------------------- L = 167.6; // outside length (X) T1 = 26.88; // slot pitch of one cell in the pack in_T1 = 24.48; // inside thickness for one plate stack (clamped) T = ns * np * T1; // block thickness, a whole number of slots H_box = 172; // box height without lid wall = 1.2; // end walls (3 perimeters at 0.4 mm) part_t = 2.4; // partition between series compartments (6 perimeters) floor_t = 2.0; // bottom thickness in_L = L - 2*wall; // 165.2 in_Tc = np * in_T1; // inside thickness of one compartment wall_y = (T - ns*in_Tc - (ns - 1)*part_t) / 2; // side walls, >= 1.2 // centre line (Y) of compartment k, k = 0 .. ns-1 function yc(k) = wall_y + k*(in_Tc + part_t) + in_Tc/2; function flipped(k) = (k % 2 == 1); rib_w = 4; rib_h = 3; rib_x = [30, 83.8, 137.6]; // rib centres rail_proud = 1.4; rail_wide = 6; // on the + end (X = 0) rail_narrow = 3; // on the - end (X = L) // ----------------------------- Lid ----------------------------------- // Lid features below are given in compartment coordinates: X along the // block, Y measured from the compartment centre line. lid_t = 3.0; lip_h = 3.0; // locating lip that drops into each compartment lip_w = 1.2; lip_gap = 0.2; // per side clearance (filled with epoxy). Between two // compartments this epoxy also seals the partition top. boss_h = 3.0; boss_hw = 12; // boss half width (Y) // + lug (titanium 4 x 25) pos_boss = [21.3, 51.3]; // X range pos_riser = [23.8, 48.8, 4.0]; // X from, X to, thickness pos_nut_x = 36.3; // - lug (brass 3 x 20) neg_boss = [118.8, 143.8]; neg_riser = [121.3, 141.3, 3.0]; neg_nut_x = 131.3; riser_y0 = -9; // riser starts 9 mm from centre sleeve = 0.6; // EPDM sleeve wall slot_clear = 0.1; well_margin = 1.5; // epoxy well around riser well_depth = 2.0; nut_y = 3; // nut / bolt 3 mm off centre nut_af = 10.1; // M6 nut across flats (press fit) nut_depth = 5.0; // leaves 1 mm floor vent_x = 70; vent_d = 14; vent_h = 3; vent_stem_d = 3.0; // umbrella valve stem hole vent_hole_d = 1.5; vent_hole_r = 4; // 4 flow holes around the stem // Set the vent numbers to match the umbrella valve you buy. The valve // stem can reach about 8 mm below the seat top before it hits the hood. fill_x = 98; fill_boss_d = 20; fill_boss_h = 3; fill_tap_d = 14.5; // tap drill for M16 x 1.5 ridge_w = 8; ridge_h = 6; // lift ridges at each end assert(ns >= 1 && np >= 1, "cells_series and cells_parallel must be >= 1"); assert(wall_y >= wall - eps, "side walls too thin"); echo(str("Layout ", ns, "S", np, "P: block ", T, " x ", L, " mm, ", ns*np, " slot(s), ~", ns*1.3, " V nominal, electrolyte ", np*359, " mL per compartment, side walls ", wall_y, " mm")); if (T > 220) echo("WARNING: block is wider than a typical 220 mm bed"); // Place children in compartment k. Odd compartments are turned 180 // degrees so their + lug lines up with the - lug of the previous cell. module comp_place(k) { if (flipped(k)) translate([L, yc(k), 0]) rotate([0, 0, 180]) children(); else translate([0, yc(k), 0]) children(); } // ===================================================================== module box() { difference() { union() { cube([L, T, H_box]); // guide rails at every slot (wide = + end, narrow = - end) for (j = [0 : ns*np - 1]) { y = (j + 0.5) * T1; translate([-rail_proud, y - rail_wide/2, 0]) cube([rail_proud + eps, rail_wide, H_box]); translate([L - eps, y - rail_narrow/2, 0]) cube([rail_proud + eps, rail_narrow, H_box]); } } for (k = [0 : ns - 1]) translate([wall, yc(k) - in_Tc/2, floor_t]) cube([in_L, in_Tc, H_box]); } // bottom ribs (plates stand on these, sludge settles between) for (k = [0 : ns - 1], x = rib_x) translate([x - rib_w/2, yc(k) - in_Tc/2 - eps, floor_t - eps]) cube([rib_w, in_Tc + 2*eps, rib_h + eps]); } // ===================================================================== module lid() { // lid local: underside at z = 0, top at z = lid_t difference() { union() { cube([L, T, lid_t]); for (k = [0 : ns - 1]) comp_place(k) comp_lid_add(); // link pads between the bosses of neighbouring cells // (guarded: OpenSCAD 2021 reverses an empty [0:-1] range) if (ns > 1) for (k = [0 : ns - 2]) link_pad(k); // lift ridges translate([0, 0, lid_t - eps]) cube([ridge_w, T, ridge_h + eps]); translate([L - ridge_w, 0, lid_t - eps]) cube([ridge_w, T, ridge_h + eps]); } for (k = [0 : ns - 1]) comp_place(k) comp_lid_cut(k); } } module comp_lid_add() { // locating lip (drops into the compartment) translate([wall + lip_gap, -in_Tc/2 + lip_gap, -lip_h]) difference() { cube([in_L - 2*lip_gap, in_Tc - 2*lip_gap, lip_h + eps]); translate([lip_w, lip_w, -eps]) cube([in_L - 2*lip_gap - 2*lip_w, in_Tc - 2*lip_gap - 2*lip_w, lip_h + 3*eps]); } // lug bosses for (b = [pos_boss, neg_boss]) translate([b[0], -boss_hw, lid_t - eps]) cube([b[1] - b[0], 2*boss_hw, boss_h + eps]); // vent seat translate([vent_x, 0, lid_t - eps]) cylinder(d = vent_d, h = vent_h + eps); // fill-port boss translate([fill_x, 0, lid_t - eps]) cylinder(d = fill_boss_d, h = fill_boss_h + eps); // splash baffle hood under the vent splash_hood(); } module comp_lid_cut(k) { // riser slots + epoxy wells lug_cut(pos_riser); lug_cut(neg_riser); // captive nut pockets (from the top, 1 mm floor left) nut_pocket(pos_nut_x); nut_pocket(neg_nut_x); // vent stem + flow holes translate([vent_x, 0, -1]) cylinder(d = vent_stem_d, h = 20); for (a = [45 : 90 : 315]) translate([vent_x + vent_hole_r*cos(a), vent_hole_r*sin(a), -1]) cylinder(d = vent_hole_d, h = 20, $fn = 24); // fill port (tap M16 x 1.5 after printing) translate([fill_x, 0, -1]) cylinder(d = fill_tap_d, h = 20); // polarity marks engraved 0.6 mm into the lid top, beside each boss translate([57, 0, lid_t - 0.6]) linear_extrude(1) text("+", size = 8, halign = "center", valign = "center"); translate([113.4, 0, lid_t - 0.6]) linear_extrude(1) text("-", size = 8, halign = "center", valign = "center"); // cell number between the vent and fill port (multi-cell blocks) if (ns > 1) translate([(vent_x + vent_d/2 + fill_x - fill_boss_d/2)/2, 0, lid_t - 0.6]) linear_extrude(1) text(str(k + 1), size = 6, halign = "center", valign = "center"); } module link_pad(k) { // Fills the gap between the - boss of cell k and the + boss of // cell k+1, so the link bar sits flat across the partition. // Even k: - lug at the far end (X ~ 131). Odd k: near end (X ~ 36). x0 = flipped(k) ? L - neg_boss[1] : neg_boss[0]; x1 = flipped(k) ? L - neg_boss[0] : neg_boss[1]; translate([x0, yc(k) + boss_hw - eps, lid_t - eps]) cube([x1 - x0, yc(k + 1) - yc(k) - 2*boss_hw + 2*eps, boss_h + eps]); } module lug_cut(r) { x0 = r[0]; x1 = r[1]; th = r[2]; s = sleeve + slot_clear; // slot through lid + boss translate([x0 - s, riser_y0 - s, -1]) cube([x1 - x0 + 2*s, th + 2*s, lid_t + boss_h + 2]); // epoxy well at the boss top translate([x0 - well_margin, riser_y0 - well_margin, lid_t + boss_h - well_depth]) cube([x1 - x0 + 2*well_margin, th + 2*well_margin, well_depth + 1]); } module nut_pocket(x) { translate([x, nut_y, lid_t + boss_h - nut_depth]) cylinder(d = nut_af / cos(30), h = nut_depth + 1, $fn = 6); } module splash_hood() { // Hood under the vent: a deflector plate at the bottom of the lip, // held by two cross walls. Gas leaves through the open sides and // rises to the vent. Splashing liquid hits the plate and drips back. hood_l = 18; hood_w = in_T1 - 2*lip_gap - 2*lip_w - 4; // clear of lip translate([vent_x - hood_l/2, -hood_w/2, -lip_h]) { cube([hood_l, hood_w, 1.0]); // plate cube([1.2, hood_w, lip_h + eps]); // wall 1 translate([hood_l - 1.2, 0, 0]) cube([1.2, hood_w, lip_h + eps]); // wall 2 } } // ===================================================================== // Hardware (display only, never part of an STL) // Lug pads: Ti 4 mm on +, Ni-brass 3 mm on -. They bend over from the // risers and lie on the boss tops over the captive nuts. // Link bars: 3 x 20 mm Ni-brass, from the - lug of cell k to the + lug // of cell k+1, across the link pad. A 1 mm Ni-brass spacer under the // bar on the - side makes up the 4 mm / 3 mm pad difference. // Bolts: M6 316 SS (M6 x 16 through a link bar, M6 x 12 / x 10 alone). pad_w = 20; // pad length in Y (riser to past nut) pad_hole_d = 6.5; bar_t = 3; bar_w = 20; bar_end = 12; spacer_t = 1; spacer_d = 14; bolt_head_af = 10; bolt_head_h = 4; z_boss_top = lid_t + boss_h; z_bar = z_boss_top + pos_riser[2]; // bar sits on the Ti pad module lug_pads() { // compartment coordinates color("LightGray") difference() { translate([pos_riser[0], riser_y0, z_boss_top]) cube([pos_riser[1] - pos_riser[0], pad_w, pos_riser[2]]); translate([pos_nut_x, nut_y, 0]) cylinder(d = pad_hole_d, h = 50); } color("Goldenrod") difference() { translate([neg_riser[0], riser_y0, z_boss_top]) cube([neg_riser[1] - neg_riser[0], pad_w, neg_riser[2]]); translate([neg_nut_x, nut_y, 0]) cylinder(d = pad_hole_d, h = 50); } } function nut_world(k, pos) = let(x = pos ? pos_nut_x : neg_nut_x) flipped(k) ? [L - x, yc(k) - nut_y] : [x, yc(k) + nut_y]; module bolt_head(xy, z) { color("DimGray") translate([xy[0], xy[1], z]) cylinder(d = bolt_head_af / cos(30), h = bolt_head_h, $fn = 6); } module hardware() { // lid coordinates (underside of the lid at z = 0) for (k = [0 : ns - 1]) comp_place(k) lug_pads(); if (ns > 1) for (k = [0 : ns - 2]) { a = nut_world(k, false); // - lug of cell k b = nut_world(k + 1, true); // + lug of cell k+1 y0 = min(a[1], b[1]) - bar_end; y1 = max(a[1], b[1]) + bar_end; color("Goldenrod") { translate([a[0], a[1], z_boss_top + neg_riser[2]]) cylinder(d = spacer_d, h = spacer_t); difference() { translate([a[0] - bar_w/2, y0, z_bar]) cube([bar_w, y1 - y0, bar_t]); for (p = [a, b]) translate([p[0], p[1], 0]) cylinder(d = pad_hole_d, h = 50); } } bolt_head(a, z_bar + bar_t); bolt_head(b, z_bar + bar_t); } // block terminals: + of cell 1 and - of the last cell bolt_head(nut_world(0, true), z_boss_top + pos_riser[2]); bolt_head(nut_world(ns - 1, false), z_boss_top + neg_riser[2]); } // ===================================================================== module fit_test() { // A short slice of the box with one lid-end piece, to check the // wall seal, lip fit and nut press-fit before the full print. intersection() { box(); translate([-5, -5, -1]) cube([60, T + 10, 25]); } translate([0, T + 10, lip_h]) intersection() { lid(); translate([-5, -5, -lip_h - 1]) cube([60, T + 10, 20]); } } // ===================================================================== if (part == "box") { box(); } else if (part == "lid") { translate([0, 0, lip_h]) lid(); // lip on the bed } else if (part == "assembly") { color("LightSteelBlue", 0.6) box(); color("SteelBlue") translate([0, 0, H_box]) lid(); if (show_hardware) translate([0, 0, H_box]) hardware(); } else if (part == "exploded") { color("LightSteelBlue", 0.6) box(); color("SteelBlue") translate([0, 0, H_box + 40]) lid(); if (show_hardware) translate([0, 0, H_box + 40]) hardware(); } else if (part == "fit_test") { fit_test(); }