#!/usr/bin/env python3 """Generate the single-cell module blueprint page (HTML + inline SVG).""" import sys OUT = sys.argv[1] def f(v): return f"{v:.2f}".rstrip("0").rstrip(".") # ---------- SVG helpers ---------- class S: def __init__(self, pid): self.p = [] self.pid = pid def add(self, s): self.p.append(s) def rect(self, x0, y0, x1, y1, cls="ol", extra=""): x, y = min(x0, x1), min(y0, y1) w, h = abs(x1 - x0), abs(y1 - y0) self.add(f'') def line(self, x0, y0, x1, y1, cls="ol"): self.add(f'') def circ(self, cx, cy, r, cls="ol"): self.add(f'') def text(self, x, y, t, cls="lbl", anchor="start", rot=None): tr = f' transform="rotate({rot} {f(x)} {f(y)})"' if rot is not None else "" self.add(f'{t}') def hdim(self, x0, x1, y, yref0, yref1, label, above=True): """Horizontal dimension at height y, extension lines from yref0/yref1.""" self.line(x0, yref0, x0, y + (-4 if above else 4), "ext") self.line(x1, yref1, x1, y + (-4 if above else 4), "ext") self.add(f'') self.text((x0 + x1) / 2, y - 4, label, "dimt", "middle") def vdim(self, x, y0, y1, xref0, xref1, label, left=True): self.line(xref0, y0, x + (4 if left else -4), y0, "ext") self.line(xref1, y1, x + (4 if left else -4), y1, "ext") self.add(f'') tx = x - 5 self.text(tx, (y0 + y1) / 2, label, "dimt", "middle", rot=-90) def balloon(self, px, py, bx, by, n): self.add(f'') self.circ(bx, by, 10, "bal") self.text(bx, by + 4, str(n), "balt", "middle") def note(self, px, py, tx, ty, t, anchor="start"): self.add(f'') self.text(tx + (4 if anchor == "start" else -4), ty + 4, t, "lbl", anchor) def svg(self, w, h, label, minw=640): defs = (f'' f'' f'' f'' f'' f'' f'' f'' f'' f'' f'' f'') body = "\n".join(self.p) return (f'' f'{defs}{body}') # ---------- Sheet 2: front elevation ---------- def front(): s = S("fe") k, ox, oy = 2.2, 120, 470 X = lambda mm: ox + mm * k Y = lambda z: oy - z * k # box and lid s.rect(X(0), Y(0), X(167.6), Y(172), "pp") s.rect(X(1.2), Y(2), X(166.4), Y(172), "inner") s.rect(X(0), Y(172), X(167.6), Y(175), "pp") # electrolyte s.rect(X(1.2), Y(2), X(166.4), Y(153), "elec") # ribs for c in (30, 83.8, 137.6): s.rect(X(c - 2), Y(2), X(c + 2), Y(5), "m-pp ol") # felt envelope (hidden edge) s.rect(X(1.3), Y(5), X(166.3), Y(148.88), "hid") # cathode plate behind (hidden) s.rect(X(3.8), Y(5.88), X(163.8), Y(145.88), "hid") # front zinc plate s.rect(X(3.8), Y(5), X(163.8), Y(145), "m-zn ol", 'fill-opacity="0.55"') # tabs s.rect(X(23.8), Y(145.88), X(48.8), Y(160), "m-cath ol") s.rect(X(118.8), Y(145), X(143.8), Y(160), "m-zn ol") # straps s.rect(X(23.8), Y(160), X(48.8), Y(166), "m-ti ol") s.rect(X(118.8), Y(160), X(143.8), Y(166), "m-brass ol") # risers s.rect(X(23.8), Y(166), X(48.8), Y(172), "m-ti ol") s.rect(X(121.3), Y(166), X(141.3), Y(172), "m-brass ol") # bosses + lug pads s.rect(X(21.3), Y(175), X(51.3), Y(178), "m-pp ol") s.rect(X(23.8), Y(178), X(48.8), Y(182), "m-ti ol") s.rect(X(118.8), Y(175), X(143.8), Y(178), "m-pp ol") s.rect(X(121.3), Y(178), X(141.3), Y(181), "m-brass ol") # vent and fill plug s.rect(X(63), Y(175), X(77), Y(178), "m-pp ol") s.add(f'') s.rect(X(90), Y(175), X(106), Y(179), "m-pp ol") s.rect(X(94), Y(179), X(102), Y(181), "m-pp ol") # lift ridges s.rect(X(0), Y(175), X(8), Y(181), "m-pp ol") s.rect(X(159.6), Y(175), X(167.6), Y(181), "m-pp ol") # fill line s.line(X(1.2), Y(153), X(166.4), Y(153), "fill") s.text(X(66), Y(153) - 6, "FILL LINE", "lbl small", "middle") # section line A-A s.line(X(83.8), Y(-6), X(83.8), Y(190), "cut") s.text(X(83.8) + 6, Y(190) + 4, "A", "cutt") s.text(X(83.8) + 8, Y(-6) + 4, "A", "cutt") # dims s.hdim(X(0), X(167.6), Y(-14), Y(0), Y(0), "167.6", above=False) s.hdim(X(3.8), X(163.8), Y(-28), Y(5), Y(5), "160 PLATE", above=False) s.vdim(X(0) - 40, Y(0), Y(175), X(0), X(0), "175") s.vdim(X(0) - 20, Y(5), Y(145), X(3.8), X(3.8), "140 PLATE") s.vdim(X(167.6) + 22, Y(2), Y(153), X(166.4), X(166.4), "151 FILL") s.vdim(X(167.6) + 44, Y(0), Y(182), X(167.6), X(48.8), "182 TO LUG") # balloons s.balloon(X(160), Y(90), X(167.6) + 75, Y(120), 1) s.balloon(X(150), Y(173.5), X(167.6) + 75, Y(200), 2) s.balloon(X(30), Y(110), X(0) - 75, Y(110), 4) s.balloon(X(40), Y(152), X(0) - 75, Y(150), 3) s.balloon(X(30), Y(163), X(0) - 75, Y(178), 6) s.balloon(X(36), Y(180), X(36), Y(207), 8) s.balloon(X(131), Y(180), X(131), Y(207), 9) s.balloon(X(135), Y(163), X(167.6) + 75, Y(165), 7) s.balloon(X(70), Y(181), X(70), Y(207), 12) s.balloon(X(98), Y(180), X(98), Y(207), 13) s.balloon(X(4), Y(60), X(0) - 75, Y(60), 5) s.balloon(X(137.6), Y(4), X(167.6) + 75, Y(20), 1) return s.svg(720, 560, "Front elevation of the cell module", 660) # ---------- Sheet 2: top view ---------- def top(): s = S("tv") k, ox, oy = 2.2, 120, 70 X = lambda mm: ox + mm * k Y = lambda y: oy + y * k s.rect(X(0), Y(0), X(167.6), Y(26.88), "pp") # rails s.rect(X(-1.4), Y(13.44 - 3), X(0), Y(13.44 + 3), "m-pp ol") s.rect(X(167.6), Y(13.44 - 1.5), X(169), Y(13.44 + 1.5), "m-pp ol") # ridges s.rect(X(0), Y(0), X(8), Y(26.88), "m-pp ol") s.rect(X(159.6), Y(0), X(167.6), Y(26.88), "m-pp ol") # bosses s.rect(X(21.3), Y(1.44), X(51.3), Y(25.44), "m-pp ol") s.rect(X(118.8), Y(1.44), X(143.8), Y(25.44), "m-pp ol") # pads s.rect(X(23.8), Y(2.44), X(48.8), Y(24.44), "m-ti ol") s.rect(X(121.3), Y(2.44), X(141.3), Y(24.44), "m-brass ol") s.circ(X(36.3), Y(16.44), 3.25 * k, "hole") s.circ(X(131.3), Y(16.44), 3.25 * k, "hole") s.text(X(36.3), Y(7.4), "+", "pol", "middle") s.text(X(131.3), Y(7.4), "−", "pol", "middle") # vent and fill s.circ(X(70), Y(13.44), 7 * k, "m-epdm ol") s.circ(X(98), Y(13.44), 8 * k, "m-pp ol") hexr = 4 * k pts = " ".join(f"{f(X(98) + hexr * c)},{f(Y(13.44) + hexr * sn)}" for c, sn in ((1, 0), (0.5, 0.866), (-0.5, 0.866), (-1, 0), (-0.5, -0.866), (0.5, -0.866))) s.add(f'') # section line s.line(X(83.8), Y(-8), X(83.8), Y(34.88), "cut") s.text(X(83.8) + 6, Y(-8) + 2, "A", "cutt") s.text(X(83.8) + 6, Y(34.88) + 10, "A", "cutt") # dims s.hdim(X(0), X(167.6), Y(-12), Y(0), Y(0), "167.6") s.hdim(X(0), X(36.3), Y(26.88) + 22, Y(26.88), Y(24.44), "36.3", above=False) s.hdim(X(0), X(70), Y(26.88) + 40, Y(26.88), Y(26.88), "70.0", above=False) s.hdim(X(0), X(98), Y(26.88) + 58, Y(26.88), Y(26.88), "98.0", above=False) s.hdim(X(0), X(131.3), Y(26.88) + 76, Y(26.88), Y(24.44), "131.3", above=False) s.vdim(X(0) - 30, Y(0), Y(26.88), X(-1.4), X(-1.4), "26.88") s.note(X(-1.4), Y(13.44), X(0) - 40, Y(-8), "WIDE RAIL 6 (+ END)") s.note(X(169), Y(13.44), X(167.6) + 30, Y(36), "NARROW RAIL 3 (− END)") s.note(X(36.3) + 7, Y(16.44) + 5, X(105), Y(-6), "Ø6.5 THRU, M6 CAPTIVE NUT") return s.svg(720, 290, "Top view of the cell module lid", 660) # ---------- Sheet 3: section A-A ---------- def section(): s = S("sa") k, ox = 22, 64 y0, y1 = 70, 290 felt = (24.48 - 14.5 - 1.2) / 10 layers = [("wall", 1.2)] for i in range(5): layers += [("zn", 0.2), ("felt", felt), ("cath", 2.9), ("felt", felt)] layers += [("zn", 0.2), ("wall", 1.2)] x = 0.0 pos = [] for kind, t in layers: xa, xb = ox + x * k, ox + (x + t) * k if kind == "wall": s.rect(xa, y0, xb, y1, "ol", f'style="fill:url(#{s.pid}pp)"') elif kind == "zn": s.rect(xa, y0, xb, y1, "m-zn ol") elif kind == "felt": s.rect(xa, y0, xb, y1, "ol", f'style="fill:url(#{s.pid}felt)"') else: s.rect(xa, y0, xb, y1, "m-cath ol") mx = (xa + xb) / 2 s.rect(mx - 0.1 * k, y0, mx + 0.1 * k, y1, "m-ti") s.line(mx, y0, mx, y1, "meshc") pos.append((kind, xa, xb)) x += t # break lines for yy in (y0, y1): s.add(f'') s.add(f'') # leaders to first unit (left side) wall = pos[0]; zn = pos[1]; fe = pos[2]; ca = pos[3] mid = (ca[1] + ca[2]) / 2 s.note((wall[1] + wall[2]) / 2, y0 + 40, ox + 10, 30, "BOX WALL 1.2 PP") s.note((zn[1] + zn[2]) / 2, y0 + 70, ox + 70, 46, "ZINC PLATE 0.20 (6)") s.note((fe[1] + fe[2]) / 2, y0 + 100, ox + 190, 30, "PP FELT 0.88 COMPRESSED (10)") s.note(mid, y0 + 130, ox + 260, 46, "TI MESH 0.2 (CENTER OF EACH CATHODE)") s.note(ca[1] + 12, y0 + 160, ox + 260, 14, "CATHODE 2.90: 75 MnO₂ / 25 C + PTFE (5)") # dims s.hdim(wall[1], wall[2], y1 + 26, y1, y1, "1.2", above=False) s.hdim(zn[1], zn[2] + 0.01, y1 + 26, y1, y1, "", above=False) s.text(zn[1] + 2, y1 + 48, "0.20", "dimt", "middle") s.hdim(fe[1], fe[2], y1 + 26, y1, y1, "0.88", above=False) s.hdim(ca[1], ca[2], y1 + 26, y1, y1, "2.90", above=False) inner0, inner1 = pos[0][2], pos[-1][1] s.hdim(inner0, inner1, y1 + 62, y1, y1, "24.48 INSIDE (STACK CLAMPED TO THIS)", above=False) s.hdim(pos[0][1], pos[-1][2], y1 + 92, y1, y1, "26.88 OVERALL", above=False) return s.svg(720, 410, "Section A-A through the plate stack", 680) # ---------- Sheet 4: plates ---------- def plates(): s = S("pl") k = 1.5 def plate(ox, base, tab_x, cls, title, pattern=None): X = lambda mm: ox + mm * k Y = lambda z: base - z * k extra = f'style="fill:url(#{s.pid}mesh)"' if pattern else "" s.rect(X(0), Y(0), X(160), Y(140), "ol" if pattern else cls + " ol", extra) s.rect(X(tab_x), Y(140), X(tab_x + 25), Y(156), cls + " ol") s.hdim(X(0), X(160), Y(0) + 26, Y(0), Y(0), "160", above=False) s.vdim(X(0) - 18, Y(0), Y(140), X(0), X(0), "140") s.hdim(X(0), X(tab_x), Y(156) - 30, Y(140), Y(156), f"{tab_x}") s.hdim(X(tab_x), X(tab_x + 25), Y(156) - 14, Y(156), Y(156), "25") s.vdim(X(160) + 18, Y(140), Y(156), X(tab_x + 25), X(tab_x + 25), "16", left=False) s.text(X(80), Y(0) + 58, title, "vtitle", "middle") plate(60, 300, 20, "m-cath", "ITEM 3 · CATHODE PLATE (×5)", pattern=True) plate(420, 300, 115, "m-zn", "ITEM 4 · ZINC PLATE (×6)") # edge detail of cathode ex, ey = 345, 160 s.rect(ex, ey, ex + 29, ey + 90, "m-cath ol") s.rect(ex + 13.5, ey, ex + 15.5, ey + 90, "m-ti") s.hdim(ex, ex + 29, ey + 108, ey + 90, ey + 90, "2.90", above=False) s.text(ex + 14.5, ey - 10, "EDGE 10:1", "lbl small", "middle") s.text(ex + 14.5, ey + 136, "1.35+0.2+1.35", "lbl small", "middle") s.text(500 + 40, 300 - 70 * 1.5, "0.20 THK, DEBURR ALL EDGES", "lbl small", "middle") return s.svg(720, 380, "Cathode and zinc plate details", 680) def envelope_and_lug(): s = S("el") # felt flat pattern, k=1 k = 1.0 ox, oy = 70, 50 s.rect(ox, oy, ox + 165 * k, oy + 290 * k, "ol", f'style="fill:url(#{s.pid}felt)"') s.rect(ox, oy, ox + 3, oy + 290, "seal") s.rect(ox + 162, oy, ox + 165, oy + 290, "seal") s.line(ox - 10, oy + 145, ox + 175, oy + 145, "fold") s.text(ox + 82.5, oy + 140, "FOLD LINE", "lbl small", "middle") s.hdim(ox, ox + 165, oy - 16, oy, oy, "165") s.vdim(ox - 22, oy, oy + 290, ox, ox, "290 FLAT") s.note(ox + 164, oy + 230, ox + 190, oy + 280, "HEAT-SEAL 2 WIDE") s.text(ox + 82.5, oy + 322, "ITEM 5 · FELT ENVELOPE (×5)", "vtitle", "middle") # lug detail section (through thickness), k=8, local y in [-16,16], z ext in [156,188] k2 = 8 lx, lz = 560, 330 # svg x of y=0, svg y of z=156 X = lambda yy: lx + yy * k2 Y = lambda z: lz - (z - 156) * k2 # lid + boss s.rect(X(-13.44), Y(172), X(13.44), Y(175), "ol", f'style="fill:url(#{s.pid}pp)"') s.rect(X(-12), Y(175), X(12), Y(178), "ol", f'style="fill:url(#{s.pid}pp)"') # epoxy well around riser s.rect(X(-10.5), Y(176), X(-3.5), Y(178), "m-epoxy ol") # gasket sleeve in slot s.rect(X(-9.6), Y(172), X(-4.4), Y(176), "m-epdm ol") # nut pocket and nut s.rect(X(-2), Y(173), X(8), Y(178), "m-ss ol") s.rect(X(1.4), Y(173), X(4.6), Y(178), "hole") # strap s.rect(X(-11), Y(160), X(11), Y(166), "m-ti ol") # riser + bend + pad s.rect(X(-9), Y(166), X(-5), Y(182), "m-ti ol") s.rect(X(-5), Y(178), X(11), Y(182), "m-ti ol") s.rect(X(-0.25), Y(178), X(6.25), Y(182), "hole") # phantom link bar + bolt s.rect(X(-6), Y(182), X(16), Y(185), "phantom") s.rect(X(0.5), Y(185), X(5.5), Y(188), "phantom") s.line(X(3), Y(188.5), X(3), Y(173), "cl") # tabs into strap for yy in (-9.708, -4.854, 0, 4.854, 9.708): s.rect(X(yy - 0.4), Y(156), X(yy + 0.4), Y(161), "m-ti ol") s.note(X(-7), Y(170), X(-16) - 30, Y(170), "TI RISER 4 THK", "end") s.note(X(-7), Y(174), X(-16) - 30, Y(175.5), "EPDM SLEEVE", "end") s.note(X(-6), Y(177.3), X(-16) - 30, Y(181), "EPOXY WELL 2 DEEP", "end") s.note(X(-11.5), Y(173.5), X(-16) - 30, Y(165), "LID 3.0 + BOSS 3.0", "end") s.note(X(0), Y(163), X(-16) - 30, Y(159), "TI STRAP 22×6×3", "end") s.note(X(7), Y(175), X(16) + 20, Y(174), "M6 HEX NUT, 316 SS") s.note(X(10), Y(180), X(16) + 20, Y(180), "PAD 4 THK, Ø6.5") s.note(X(15), Y(183.5), X(16) + 20, Y(186), "LINK BAR (PHANTOM)") s.text(lx, Y(156) + 40, "DETAIL C · + LUG FEEDTHROUGH 8:1", "vtitle", "middle") s.text(lx, Y(156) + 58, "− LUG SAME LAYOUT IN NI-PLATED BRASS 3 THK", "lbl small", "middle") return s.svg(860, 420, "Felt envelope flat pattern and terminal lug detail", 760) FRONT, TOP, SEC, PLATES, ENV = front(), top(), section(), plates(), envelope_and_lug() # ---------- Sheet 8: 2S2P block ---------- # Geometry from cell_module_case.scad (cells_series = 2, cells_parallel = 2) BL, BT = 167.6, 107.52 WALL_Y, PART_T, IN_TC = 3.6, 2.4, 48.96 YC = [WALL_Y + IN_TC / 2, WALL_Y + IN_TC + PART_T + IN_TC / 2] # 28.08, 79.44 def block_top(): s = S("bt") k, ox, oy = 3.0, 230, 80 X = lambda mm: ox + mm * k Y = lambda y: oy + y * k s.rect(X(0), Y(0), X(BL), Y(BT), "pp") # rails at every slot for j in range(4): yc = (j + 0.5) * 26.88 s.rect(X(-1.4), Y(yc - 3), X(0), Y(yc + 3), "m-pp ol") s.rect(X(BL), Y(yc - 1.5), X(BL + 1.4), Y(yc + 1.5), "m-pp ol") # ridges s.rect(X(0), Y(0), X(8), Y(BT), "m-pp ol") s.rect(X(BL - 8), Y(0), X(BL), Y(BT), "m-pp ol") # hidden: compartments and partition for c in range(2): s.rect(X(1.2), Y(YC[c] - IN_TC / 2), X(BL - 1.2), Y(YC[c] + IN_TC / 2), "hid") # cell 1 (normal) and cell 2 (turned 180) y1, y2 = YC # bosses s.rect(X(21.3), Y(y1 - 12), X(51.3), Y(y1 + 12), "m-pp ol") # cell 1 + s.rect(X(118.8), Y(y1 - 12), X(143.8), Y(y1 + 12), "m-pp ol") # cell 1 - s.rect(X(118.8), Y(y1 + 12), X(143.8), Y(y2 - 12), "m-pp ol") # link pad s.rect(X(116.3), Y(y2 - 12), X(146.3), Y(y2 + 12), "m-pp ol") # cell 2 + s.rect(X(23.8), Y(y2 - 12), X(48.8), Y(y2 + 12), "m-pp ol") # cell 2 - # lug pads s.rect(X(23.8), Y(y1 - 9), X(48.8), Y(y1 + 11), "m-ti ol") s.rect(X(121.3), Y(y1 - 9), X(141.3), Y(y1 + 11), "m-brass ol") s.rect(X(118.8), Y(y2 - 11), X(143.8), Y(y2 + 9), "m-ti ol") s.rect(X(26.3), Y(y2 - 11), X(46.3), Y(y2 + 9), "m-brass ol") # link bar (over the pads) s.rect(X(121.3), Y(y1 + 3 - 12), X(141.3), Y(y2 - 3 + 12), "m-brass ol", 'style="fill-opacity:.55"') for (x, y) in ((36.3, y1 + 3), (131.3, y1 + 3), (131.3, y2 - 3), (36.3, y2 - 3)): s.circ(X(x), Y(y), 3.25 * k, "hole") # vents, fills, marks for (vx, fx, y) in ((70, 98, y1), (BL - 70, BL - 98, y2)): s.circ(X(vx), Y(y), 7 * k, "m-epdm ol") s.circ(X(fx), Y(y), 8 * k, "m-pp ol") hexr = 4 * k pts = " ".join(f"{f(X(fx) + hexr * c)},{f(Y(y) + hexr * sn)}" for c, sn in ((1, 0), (0.5, 0.866), (-0.5, 0.866), (-1, 0), (-0.5, -0.866), (0.5, -0.866))) s.add(f'') s.text(X(82.5), Y(y1) + 5, "1", "pol", "middle") s.text(X(BL - 82.5), Y(y2) + 5, "2", "pol", "middle") s.text(X(57), Y(y1) + 5, "+", "pol", "middle") s.text(X(113.4), Y(y1) + 5, "−", "pol", "middle") s.text(X(BL - 57), Y(y2) + 5, "+", "pol", "middle") s.text(X(BL - 113.4), Y(y2) + 5, "−", "pol", "middle") # section line B-B through the link bar s.line(X(131.3), Y(-8), X(131.3), Y(BT + 8), "cut") s.text(X(131.3) + 6, Y(-8) + 2, "B", "cutt") s.text(X(131.3) + 6, Y(BT + 8) + 10, "B", "cutt") # dims s.hdim(X(0), X(BL), Y(-14), Y(0), Y(0), "167.6") s.vdim(X(BL) + 100, Y(0), Y(BT), X(BL + 1.4), X(BL + 1.4), "107.52 = 4 SLOTS", left=False) s.vdim(X(BL) + 44, Y(y1 + 3), Y(y2 - 3), X(141.3), X(141.3), "45.36", left=False) # notes s.note(X(36.3) - 8, Y(y1 + 3), X(-1.4) - 20, Y(y1 - 14), "BLOCK + (CELL 1, TI)", "end") s.note(X(36.3) - 8, Y(y2 - 3), X(-1.4) - 20, Y(y2 + 14), "BLOCK − (CELL 2, BRASS)", "end") s.note(X(141.3), Y((y1 + y2) / 2), X(BL) - 10, Y(BT + 18), "LINK BAR 3 × 20, NI-BRASS") s.note(X(60), Y(y1 + IN_TC / 2 + 1.2), X(30), Y(BT + 18), "PARTITION 2.4 (HIDDEN)") return s.svg(960, 480, "Lid plan of the 2S2P block", 760) def block_section(): s = S("bs") kh, kv, ox, oy = 4.2, 1.9, 150, 40 X = lambda y: ox + y * kh # block thickness across the page Z = lambda z: oy + (190 - z) * kv # height up the page # electrolyte, walls, floor, partition for c in range(2): a, b = YC[c] - IN_TC / 2, YC[c] + IN_TC / 2 s.rect(X(a), Z(153), X(b), Z(2), "elec") s.rect(X(0), Z(172), X(WALL_Y), Z(0), "ol", f'style="fill:url(#{s.pid}pp)"') s.rect(X(BT - WALL_Y), Z(172), X(BT), Z(0), "ol", f'style="fill:url(#{s.pid}pp)"') s.rect(X(WALL_Y + IN_TC), Z(172), X(WALL_Y + IN_TC + PART_T), Z(0), "ol", f'style="fill:url(#{s.pid}pp)"') s.rect(X(0), Z(2), X(BT), Z(0), "ol", f'style="fill:url(#{s.pid}pp)"') s.line(X(WALL_Y), Z(153), X(BT - WALL_Y), Z(153), "fill") # plate stacks: 2 per compartment, cut through the tab area for c in range(2): a = YC[c] - IN_TC / 2 for st in range(2): x0 = a + st * 24.48 s.rect(X(x0 + 0.3), Z(145), X(x0 + 24.18), Z(5), "m-cath ol") for i in range(6): # zinc plates zx = x0 + 0.3 + i * 4.776 s.line(X(zx), Z(145), X(zx), Z(5), "ol") s.rect(X(YC[c]) - 78, Z(80) - 18, X(YC[c]) + 78, Z(80) + 24, "hole") s.text(X(YC[c]), Z(80), f"CELL {c + 1}", "vtitle", "middle") s.text(X(YC[c]), Z(80) + 16, "2 STACKS IN PARALLEL", "lbl small", "middle") # straps and risers at X = 131.3: cell 1 zinc (brass), cell 2 cathode (Ti) s.rect(X(YC[0] - 23), Z(166), X(YC[0] + 23), Z(160), "m-brass ol") s.rect(X(YC[1] - 23), Z(166), X(YC[1] + 23), Z(160), "m-ti ol") for c in range(2): for xs in [YC[c] - 23 + 2 + i * 3.6 for i in range(13)]: s.line(X(xs), Z(160), X(xs), Z(145), "ol") s.rect(X(YC[0] - 9), Z(178), X(YC[0] - 6), Z(166), "m-brass ol") # riser 3 s.rect(X(YC[1] + 5), Z(178), X(YC[1] + 9), Z(166), "m-ti ol") # riser 4 # lid with lips, bosses + link pad s.rect(X(0), Z(175), X(BT), Z(172), "m-pp ol") for c in range(2): a, b = YC[c] - IN_TC / 2, YC[c] + IN_TC / 2 s.rect(X(a + 0.2), Z(172), X(a + 1.4), Z(169), "m-pp ol") s.rect(X(b - 1.4), Z(172), X(b - 0.2), Z(169), "m-pp ol") s.rect(X(YC[0] - 12), Z(178), X(YC[1] + 12), Z(175), "m-pp ol") # epoxy seal over the partition top s.rect(X(WALL_Y + IN_TC - 0.2), Z(172), X(WALL_Y + IN_TC + PART_T + 0.2), Z(170.5), "m-epoxy ol") # lug pads, spacer, link bar, bolts s.rect(X(YC[0] - 9), Z(181), X(YC[0] + 11), Z(178), "m-brass ol") s.rect(X(YC[1] - 11), Z(182), X(YC[1] + 9), Z(178), "m-ti ol") s.rect(X(YC[0] + 3 - 7), Z(182), X(YC[0] + 3 + 7), Z(181), "m-brass ol") s.rect(X(YC[0] + 3 - 12), Z(185), X(YC[1] - 3 + 12), Z(182), "m-brass ol") for yb in (YC[0] + 3, YC[1] - 3): s.rect(X(yb - 5), Z(189), X(yb + 5), Z(185), "m-ss ol") s.line(X(yb), Z(191), X(yb), Z(172), "cl") # notes s.note(X(YC[0] + 3) + 22, Z(184), X(YC[0] + 3) + 60, Z(203), "LINK BAR 3 THK") s.note(X(YC[0] + 3) - 20, Z(181.5), X(0) - 8, Z(196), "1 MM SPACER", "end") s.note(X(YC[0] - 7.5), Z(172), X(0) - 8, Z(176), "− RISER, BRASS 3", "end") s.note(X(YC[0] - 15), Z(163), X(0) - 8, Z(158), "BRASS STRAP 46 LONG", "end") s.note(X(WALL_Y + IN_TC + PART_T / 2), Z(171), X(BT) + 10, Z(196), "EPOXY SEALS PARTITION TOP") s.note(X(YC[1] + 7), Z(172), X(BT) + 10, Z(176), "+ RISER, TI 4") s.note(X(YC[1] + 15), Z(163), X(BT) + 10, Z(158), "TI STRAP 46 LONG") s.note(X(BT - 1.8), Z(120), X(BT) + 10, Z(120), "SIDE WALL 3.6") s.note(X(WALL_Y + 10), Z(153), X(0) - 8, Z(140), "FILL LINE", "end") # dims s.hdim(X(0), X(BT), Z(0) + 40, Z(0), Z(0), "107.52", above=False) s.hdim(X(WALL_Y), X(WALL_Y + IN_TC), Z(0) + 20, Z(0), Z(0), "48.96", above=False) s.hdim(X(WALL_Y + IN_TC + PART_T), X(BT - WALL_Y), Z(0) + 20, Z(0), Z(0), "48.96", above=False) s.text(X(WALL_Y + IN_TC + PART_T / 2), Z(0) + 16, "2.4", "dimt", "middle") return s.svg(820, 470, "Section B-B through the 2S2P block at the link bar", 720) def block_schematic(): s = S("bk") def cellsym(x, y): s.line(x, y - 14, x, y + 14, "ol") s.add(f'') s.line(x - 14, y, x, y, "ol"); s.line(x + 8, y, x + 22, y, "ol") def group(x, label): for y in (40, 90): cellsym(x + 30, y) s.line(x + 16, 40, x + 16, 90, "ol"); s.line(x + 52, 40, x + 52, 90, "ol") s.line(x, 65, x + 16, 65, "ol"); s.line(x + 52, 65, x + 68, 65, "ol") s.text(x + 34, 125, label, "lbl", "middle") s.circ(40, 65, 5, "ol"); s.text(40, 52, "+", "pol", "middle") s.line(45, 65, 80, 65, "ol") group(80, "CELL 1 (2P)") s.line(148, 65, 210, 65, "ol"); s.text(179, 56, "LINK", "lbl small", "middle") group(210, "CELL 2 (2P)") s.line(278, 65, 315, 65, "ol"); s.circ(320, 65, 5, "ol"); s.text(320, 52, "−", "pol", "middle") s.text(180, 150, "+ IS THE TI LUG (CATHODE), − IS THE BRASS LUG (ZINC)", "lbl small", "middle") return s.svg(360, 165, "Schematic of the 2S2P block", 320) def pack_plan(): s = S("pk") k, ox, oy = 0.95, 60, 60 X = lambda mm: ox + mm * k Y = lambda y: oy + y * k for b in range(5): y0 = b * BT s.rect(X(0), Y(y0), X(BL), Y(y0 + BT), "pp") s.rect(X(0), Y(y0 + 3.6 + 48.96), X(BL), Y(y0 + 3.6 + 48.96 + 2.4), "m-pp ol") s.text(X(84), Y(y0 + YC[0]) + 4, f"BLOCK {b + 1}", "lbl", "middle") s.circ(X(36.3), Y(y0 + YC[0] + 3), 5, "m-ti ol") s.circ(X(36.3), Y(y0 + YC[1] - 3), 5, "m-brass ol") s.rect(X(131.3) - 5, Y(y0 + YC[0] + 3) - 3, X(131.3) + 5, Y(y0 + YC[1] - 3) + 3, "m-brass ol") for b in range(4): # block - to next block +, drawn over the blocks y0 = b * BT s.rect(X(36.3) - 3, Y(y0 + YC[1] - 3), X(36.3) + 3, Y(y0 + BT + YC[0] + 3), "m-brass ol") for b in range(5): s.circ(X(36.3), Y(b * BT + YC[0] + 3), 5, "m-ti ol") s.circ(X(36.3), Y(b * BT + YC[1] - 3), 5, "m-brass ol") s.text(X(36.3) - 12, Y(YC[0] + 3) + 4, "PACK +", "lbl", "end") s.text(X(36.3) - 12, Y(4 * BT + YC[1] - 3) + 4, "PACK −", "lbl", "end") s.hdim(X(0), X(BL), Y(0) - 18, Y(0), Y(0), "167.6") s.vdim(X(BL) + 34, Y(0), Y(5 * BT), X(BL), X(BL), "537.6 = 20 SLOTS", left=False) s.text(X(BL) + 60, Y(2.5 * BT) - 40, "INTER-BLOCK BARS", "lbl") s.text(X(BL) + 60, Y(2.5 * BT) - 24, "AT THE + END (X = 0),", "lbl") s.text(X(BL) + 60, Y(2.5 * BT) - 8, "62.2 CENTERS", "lbl") s.text(X(BL) + 60, Y(2.5 * BT) + 20, "10S2P: ~13 V", "lbl") s.text(X(BL) + 60, Y(2.5 * BT) + 36, "80–140 AH (EST.)", "lbl") return s.svg(480, 600, "Plan of a 10S2P pack made from five 2S2P blocks", 420) BTOP, BSEC, BSCH, BPACK = block_top(), block_section(), block_schematic(), pack_plan() BOM = [ ("1", "Module box", "Polypropylene", "167.6 × 26.88 × 172 ext. 1.2 walls, 2.0 bottom, three 4 × 3 ribs. Guide rails: 6 wide on + end, 3 wide on − end, 1.4 proud", "1", "≈ 85 g"), ("2", "Lid", "Polypropylene", "167.6 × 26.88 × 3.0, two lug bosses 3.0 high, vent seat, fill thread, lift ridges 8 wide × 6 high", "1", "Hot-plate welded"), ("3", "Cathode plate", "75% MnO₂ / 25% carbon + 4% PTFE on Ti mesh", "160 × 140 × 2.90, 50% porosity, tab 25 × 16", "5", "90.91 g MnO₂ + 30.30 g C + ≈ 4.8 g PTFE"), ("3a", "Cathode collector", "Expanded titanium mesh, Gr 2", "160 × 140 × 0.2 with solid tab 25 × 16", "5", "≈ 8.1 g"), ("4", "Zinc plate", "99% zinc sheet", "160 × 140 × 0.20, tab 25 × 16", "6", "32.0 g + tab"), ("5", "Felt envelope", "PP needle felt ≈ 1.2 thick, Triton X‑100 treated", "Flat 165 × 290, folded, sides heat-sealed 2 wide. 0.88 compressed", "5", ""), ("6", "Cathode strap", "Titanium Gr 2", "22 × 6 × 3", "1", "Welds to 5 tabs + item 8"), ("7", "Anode strap", "Nickel-plated brass", "22 × 6 × 3", "1", "Solder or weld to 6 tabs + item 9"), ("8", "+ terminal lug", "Titanium Gr 2", "4 × 25 riser, bent to 20 × 25 pad, Ø6.5 hole", "1", "≈ 0.13 mΩ"), ("9", "− terminal lug", "Nickel-plated brass", "3 × 20 riser, bent to 20 × 20 pad, Ø6.5 hole", "1", ""), ("10", "Captive nut", "316 stainless", "M6 hex, pressed into boss", "2", ""), ("11", "Lug seal", "EPDM sleeve + epoxy", "Sleeve in lid slot, epoxy well 2 deep", "2", ""), ("12", "Vent valve", "EPDM umbrella valve", "Opens 1.5–2.5 psi, splash baffle molded under lid", "1", ""), ("13", "Fill plug", "Polypropylene + EPDM O-ring", "Ø16 head, threaded into lid", "1", ""), ("14", "Electrolyte", "2 M ZnSO₄ + 0.1 M MnSO₄", "359 mL, fill to 151 above inside floor", "1", "≈ 500 g"), ("15", "Barrier film (optional)", "Cellophane, 25 µm", "Envelope 165 × 300 over item 5 (zinc side)", "5", "Felt drops to ≈ 0.85"), ] STEPS = [ ("Treat the felt", "Soak felt in 0.1–0.5 wt% Triton X‑100 in isopropyl alcohol for 5 minutes. Hang until fully dry.", "Water drop soaks in within seconds"), ("Cut the zinc plates", "Cut six 160 × 140 plates with a 25 × 16 tab at 115 from the left edge. Deburr every edge and degrease with alcohol.", "0.20 ± 0.02 thick, no burrs by fingertip"), ("Mix the cathode", "Per plate: dry-blend 90.91 g MnO₂ with 30.30 g carbon, then knead in PTFE dispersion (≈ 4.8 g solids) to a dough.", "Uniform black, no dry streaks"), ("Form the cathode plates", "Roll the dough to two 1.35 sheets, lay one each side of the Ti mesh (tab at 20 from the left), and press or roll to 2.90.", "2.90 ± 0.05 thick, ≈ 134 g with mesh"), ("Make the envelopes", "Fold each 165 × 290 felt around a cathode plate, fold at the bottom edge, and heat-seal both sides 2 wide. Add the optional cellophane envelope over it.", "No exposed cathode edge"), ("Stack the cell", "Zinc, cathode, zinc, cathode … ending in zinc (6 zinc, 5 cathode). Line up the cathode tabs on the left and zinc tabs on the right.", "Tabs never cross over"), ("Clamp", "Squeeze the stack between two flat plates to 24.48 and hold it with a clamp.", "24.48 ± 0.1 across the full face"), ("Weld the straps", "Weld the 5 cathode tabs to the Ti strap and the 6 zinc tabs to the brass strap. Weld the + riser to the Ti strap and the − riser to the brass strap.", "No cathode-to-zinc short (meter reads open)"), ("Box the stack", "Slide the clamped stack into the box until it rests on the ribs, then release the clamp.", "Plates sit square, felt touches both walls"), ("Fit the lid", "Thread the risers through the lid slots with EPDM sleeves, hot-plate weld the lid, bend the risers over the bosses onto the captive nuts, and fill the wells with epoxy.", "Lugs flat, holes line up with nuts"), ("Leak test", "With the vent capped, pressurize through the fill port to 1.0 psi and brush soapy water over every seam and lug.", "No bubbles for 5 minutes"), ("Fill", "Add 359 mL of electrolyte in 3–4 pours, 15 minutes apart, up to the fill line. Fit the plug.", "Level at 151 above the inside floor"), ("Wet out", "Let the cell rest for 24 hours.", "Rest voltage steady, about 1.3–1.5 V"), ("Form", "Cycles 1–2: 1.4 A discharge to 14 Ah or 1.05 V, charge at 1.4 A. Cycles 3–4: 3.5 A discharge to 35 Ah or 0.90 V, CC/CV charge. Record the capacity of each cycle.", "Capacity rises or holds each cycle"), ("Baseline", "Charge fully, rest 24 hours, and log the voltage drop. This is the self-discharge figure to compare against later.", "Logged with the cell serial number"), ] def esc(t): return t.replace("&", "&").replace("<", "<") if False else t bom_rows = "\n".join( f"{a}{b}{c}{d}{e}{g}" for a, b, c, d, e, g in BOM) step_rows = "\n".join( f"
  • {t}

    {d}

    Check{q}
  • " for t, d, q in STEPS) HTML = f"""Zn‑MnO₂ Cell Module Blueprints
    Drawing set ZM‑CELL‑01 · Rev C · Units: mm unless noted · Sheet 8: 2S2P block

    Zn‑MnO₂ Cell Module

    Construction drawings for one replaceable cell of the 12 V, 10-cell salt-water battery: a sealed polypropylene box holding 5 cathode plates and 6 zinc plates in compressed felt, with bolt-hole terminal lugs on the lid.

    Nominal voltage
    1.3 V
    Capacity
    70 Ahdesign target · 40–70 Ah estimated
    Outside size
    167.6 × 26.88 × 175+ lugs to 182
    Mass
    ≈ 1.52 kgfilled
    Resistance
    3–6 mΩestimated
    Voltage window
    0.90–1.50 Vcharge limit unverified, see sheet 7

    General arrangement

    Front elevation looking through the front wall, with the nearest zinc plate shaded. Balloons match the item numbers in the bill of materials on sheet 6.

    Polypropylene Zinc Cathode mix Titanium Ni-plated brass PP felt Electrolyte
    {FRONT}

    Height budget (inside, 170)

    Bottom ribs3
    Plates140
    Felt overhang above cathode3
    Electrolyte above felt≈ 5
    Tabs, straps, risers19

    Plate positions

    Plates are centered across the 165.2 inside width, leaving 2.6 each side. Zinc plates stand on the ribs. Cathode plates sit 0.88 higher, on the folded felt. Cathode tabs are 20 from the left (+) end, zinc tabs 115 from the left.

    TitleGeneral arrangement · front elevation
    Sheet2 of 8
    Scale1:1 approx. on screen
    RevC

    Lid plan

    Seen from above. The wide guide rail marks the + end, so the module can only go into its slot one way. The vent and fill port sit on the centerline. The lug holes sit 3 off center so each riser can pass through the lid beside its nut.

    {TOP}
    TitleLid plan · terminals, vent, fill port
    Sheet3 of 8
    Scale1:1 approx. on screen
    RevC

    Section A‑A · plate stack

    Cut through the middle of the cell, looking toward the + end. Thickness is drawn about 9 times larger than the elevation so every layer shows. Zinc plates are at both walls, so each of the 10 cathode faces faces zinc.

    {SEC}
    TitleSection A‑A · layer stack
    Sheet4 of 8
    Scale≈ 20:1 horizontal
    RevC

    Part details

    {PLATES}
    {ENV}

    The + lug riser comes up beside the strap center, passes through an EPDM sleeve in the lid slot, and bends over the boss so its hole sits above the captive M6 nut. The epoxy well around the riser is the main seal. Weld titanium only to titanium.

    TitlePlates, felt envelope, lug feedthrough
    Sheet5 of 8
    ScaleAs noted
    RevC

    Bill of materials · one cell

    {bom_rows}
    ItemPartMaterialSizeQtyMass / note

    Electrolyte batch · 400 mL

    Zinc sulfate heptahydrate230.0 g
    Manganese sulfate monohydrate6.76 g
    Distilled waterto 400 mL

    Dissolve the manganese sulfate first, then the zinc sulfate in batches (the mix turns cold). Top up to 400 mL once back at room temperature. Use 359 mL in the cell.

    Active material per cell

    MnO₂454.54 g
    Carbon powder151.51 g
    PTFE binder (4%)≈ 24 g
    Zinc191.9 g
    Titanium mesh≈ 40 g
    TitleBill of materials
    Sheet6 of 8
    ScaleNone
    RevC

    Build sequence and checks

      {step_rows}

    Open items before building a full pack

    • Charge voltage. The design limits charge to 1.50 V. Published Zn‑MnO₂ cells in sulfate electrolyte usually charge to 1.8–1.9 V. Form this first cell at 1.50 V, then try 1.80 V and compare recovered capacity.
    • Felt width. The envelope is 165 wide. Earlier notes said 166, which does not fit the 165.2 inside width, and note.txt is now corrected.
    • Cold limit. The electrolyte freezes at about −6 °C. Keep the cell above that.
    • Pressing force. A full 160 × 140 plate pressed flat at once needs tens of tonnes. Roll the sheets or press in tiles.
    TitleAssembly procedure · quality checks
    Sheet7 of 8
    ScaleNone
    RevC

    2S2P block · four-cell variant

    Two sealed compartments in one printed box, each holding two standard plate stacks in parallel. Cell 2 is turned 180°, so its + lug sits beside the − lug of cell 1 and a short link bar on top of the lid joins them. Plate stacks, felt, straps, lugs and seals are the same parts as the single module (sheets 2–7) unless noted here. Model: cell_module_case.scad with cells_series = 2, cells_parallel = 2.

    {BTOP}
    {BSEC}

    Block data

    Nominal voltage2.6 V
    Capacity (estimate)80–140 Ah
    Outside size167.6 × 107.52 × 175
    Slots in the pack case4
    Compartment inside165.2 × 48.96 × 170
    Electrolyte718 mL × 2
    Mass, filled≈ 6.1 kg
    Resistance (estimate)3–6 mΩ
    Printed case (PETG)≈ 550 g

    Schematic

    {BSCH}

    Parts that change

    Plate stack kits (items 3–5)4
    Ti strap / brass strap, 46 × 6 × 32 / 2
    + lugs / − lugs (items 8, 9)2 / 2
    Link bar, Ni-brass 3 × 20 × 691
    Spacer, Ni-brass Ø14 × 11
    Captive nuts, M6 316 SS4
    Bolts: M6 × 16 (link) / M6 × 12, × 102 / 1, 1
    Vent valves / fill plugs2 / 2

    10S2P pack from five blocks

    {BPACK}

    Build differences

    1. Leak test the empty box, then fill one compartment at a time with water and check the other stays dry for 24 h. The partition must never be drilled or notched.
    2. Clamp the two stacks of each cell side by side to 48.96. Join all 10 cathode tabs to one 46-long Ti strap and all 12 zinc tabs to one 46-long brass strap.
    3. Turn the cell 2 stack 180° before boxing it, so its cathode tabs are at the − end of the block.
    4. Fit one lid over both compartments. Epoxy fills every lip gap; check the bead closes over the partition top, because it is the only seal between the cells.
    5. Bolt the link bar across the link pad with the 1 mm spacer on the brass side, then measure each cell separately: 1.3–1.5 V each, 2.6–3.0 V across the block.
    6. Fill each compartment with 718 mL through its own port.

    Pack size

    A 10S2P pack needs 20 slots (537.6), twice the length of the original 10-slot case. Use a longer case or two 10-slot cases side by side.

    Title2S2P block · lid, section B‑B, pack layout
    Sheet8 of 8
    ScaleAs drawn
    RevD
    """ open(OUT, "w").write(HTML) print("ok", len(HTML))