Former/battery.go

461 lines
15 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
)
// Standard cylindrical cell dimensions: diameter × length (mm)
var StandardCells = map[string][2]float64{
"10440": {10, 44},
"14500": {14, 50},
"16340": {16, 34},
"18350": {18, 35},
"18650": {18, 65},
"20700": {20, 70},
"21700": {21, 70},
"26650": {26, 65},
"32650": {32, 65},
}
// BatteryConfig defines a battery compartment to be generated inside an enclosure.
type BatteryConfig struct {
CellSize string `json:"cellSize"` // preset key or "custom"
CellDiameter float64 `json:"cellDiameter"` // mm, used when CellSize == "custom"
CellLength float64 `json:"cellLength"` // mm, used when CellSize == "custom"
Series int `json:"series"`
Parallel int `json:"parallel"`
PositionX float64 `json:"positionX"` // bay origin X in enclosure coords
PositionY float64 `json:"positionY"` // bay origin Y in enclosure coords
Rotation float64 `json:"rotation"` // degrees, placement rotation on the canvas
Placement string `json:"placement"` // "bottom", "side-1", "side-2", etc.
Orientation string `json:"orientation"` // "horizontal" or "vertical"
WallThick float64 `json:"wallThick"` // internal wall thickness (default 1.2)
Clearance float64 `json:"clearance"` // gap around each cell (default 0.3)
HasLid bool `json:"hasLid"` // generate a clip-on lid for the bay
RoutingHoles []BatteryRoutingHole `json:"routingHoles"`
}
// BatteryRoutingHole is a wire passthrough in the bay's outer walls or floor
// to route power wires from the battery bay to the PCB.
type BatteryRoutingHole struct {
Face string `json:"face"` // "front", "back", "left", "right", "floor"
Position float64 `json:"position"` // 0..1 along the face width
Height float64 `json:"height"` // 0..1 along the face height (ignored for floor)
Diameter float64 `json:"diameter"` // hole diameter mm
}
// BatteryLayout holds computed dimensions for a battery bay.
type BatteryLayout struct {
BayWidth float64 `json:"bayWidth"`
BayDepth float64 `json:"bayDepth"`
BayHeight float64 `json:"bayHeight"`
Rows int `json:"rows"`
Cols int `json:"cols"`
CellDia float64 `json:"cellDia"`
CellLen float64 `json:"cellLen"`
Wall float64 `json:"wall"`
Clr float64 `json:"clr"`
TotalCells int `json:"totalCells"`
}
func (cfg *BatteryConfig) resolveDimensions() (dia, length float64) {
if dims, ok := StandardCells[cfg.CellSize]; ok {
return dims[0], dims[1]
}
return cfg.CellDiameter, cfg.CellLength
}
func (cfg *BatteryConfig) wallThick() float64 {
if cfg.WallThick > 0 {
return cfg.WallThick
}
return 1.2
}
func (cfg *BatteryConfig) clearance() float64 {
if cfg.Clearance > 0 {
return cfg.Clearance
}
return 0.3
}
// ComputeLayout calculates the physical dimensions of the battery bay.
func (cfg *BatteryConfig) ComputeLayout() BatteryLayout {
dia, length := cfg.resolveDimensions()
wall := cfg.wallThick()
clr := cfg.clearance()
rows := cfg.Series
cols := cfg.Parallel
if rows < 1 {
rows = 1
}
if cols < 1 {
cols = 1
}
cellSlot := dia + 2*clr
var bayW, bayD, bayH float64
if cfg.Orientation == "vertical" {
bayW = float64(cols)*(cellSlot+wall) + wall
bayD = float64(rows)*(cellSlot+wall) + wall
bayH = length + clr + wall
} else {
// horizontal: cell axis along X
bayW = length + 2*clr + 2*wall
bayD = float64(cols)*(cellSlot+wall) + wall
bayH = float64(rows)*(cellSlot+wall) + wall
}
return BatteryLayout{
BayWidth: bayW,
BayDepth: bayD,
BayHeight: bayH,
Rows: rows,
Cols: cols,
CellDia: dia,
CellLen: length,
Wall: wall,
Clr: clr,
TotalCells: rows * cols,
}
}
// WriteBatteryBaySCAD writes the battery bay base piece to a file.
func WriteBatteryBaySCAD(filename string, cfg *BatteryConfig) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return writeBatteryBaySCADTo(f, cfg)
}
// GenerateBatteryBaySCADString returns the bay SCAD as a string.
func GenerateBatteryBaySCADString(cfg *BatteryConfig) (string, error) {
var buf bytes.Buffer
if err := writeBatteryBaySCADTo(&buf, cfg); err != nil {
return "", err
}
return buf.String(), nil
}
func writeBatteryBaySCADTo(f io.Writer, cfg *BatteryConfig) error {
lay := cfg.ComputeLayout()
wall := lay.Wall
clr := lay.Clr
cellSlot := lay.CellDia + 2*clr
fmt.Fprintf(f, "// Battery bay — %ds%dp %s cells (%s)\n", lay.Rows, lay.Cols, cfg.CellSize, cfg.Orientation)
fmt.Fprintf(f, "$fn = 48;\n\n")
fmt.Fprintf(f, "cell_dia = %f;\n", lay.CellDia)
fmt.Fprintf(f, "cell_len = %f;\n", lay.CellLen)
fmt.Fprintf(f, "wall = %f;\n", wall)
fmt.Fprintf(f, "clr = %f;\n", clr)
fmt.Fprintf(f, "cell_slot = cell_dia + 2*clr;\n")
fmt.Fprintf(f, "bay_w = %f;\n", lay.BayWidth)
fmt.Fprintf(f, "bay_d = %f;\n", lay.BayDepth)
fmt.Fprintf(f, "bay_h = %f;\n\n", lay.BayHeight)
if cfg.Orientation == "vertical" {
writeVerticalBay(f, lay, cellSlot, wall, clr, cfg)
} else {
writeHorizontalBay(f, lay, cellSlot, wall, clr, cfg)
}
return nil
}
func writeHorizontalBay(f io.Writer, lay BatteryLayout, cellSlot, wall, clr float64, cfg *BatteryConfig) {
cellCavityLen := lay.CellLen + 2*clr
fmt.Fprintf(f, "// Battery bay base — cells lie horizontal, open top for insertion\n")
fmt.Fprintf(f, "difference() {\n")
fmt.Fprintf(f, " cube([bay_w, bay_d, bay_h]);\n\n")
for row := 0; row < lay.Rows; row++ {
for col := 0; col < lay.Cols; col++ {
cy := wall + cellSlot/2 + float64(col)*(cellSlot+wall)
cz := wall + cellSlot/2 + float64(row)*(cellSlot+wall)
fmt.Fprintf(f, " // Cell [%d,%d]\n", row, col)
fmt.Fprintf(f, " translate([wall, %f, %f])\n", cy, cz)
fmt.Fprintf(f, " rotate([0, 90, 0])\n")
fmt.Fprintf(f, " cylinder(d=cell_slot, h=%f);\n", cellCavityLen)
// Open above cell center for top insertion
fmt.Fprintf(f, " translate([wall, %f, %f])\n", cy-cellSlot/2, cz)
fmt.Fprintf(f, " cube([%f, cell_slot, bay_h]);\n\n", cellCavityLen)
}
}
writeRoutingHoles(f, lay, cfg)
fmt.Fprintf(f, "}\n")
}
func writeVerticalBay(f io.Writer, lay BatteryLayout, cellSlot, wall, clr float64, cfg *BatteryConfig) {
fmt.Fprintf(f, "// Battery bay base — cells stand vertical\n")
fmt.Fprintf(f, "difference() {\n")
fmt.Fprintf(f, " cube([bay_w, bay_d, bay_h]);\n\n")
for row := 0; row < lay.Rows; row++ {
for col := 0; col < lay.Cols; col++ {
cx := wall + cellSlot/2 + float64(col)*(cellSlot+wall)
cy := wall + cellSlot/2 + float64(row)*(cellSlot+wall)
fmt.Fprintf(f, " // Cell [%d,%d]\n", row, col)
fmt.Fprintf(f, " translate([%f, %f, wall])\n", cx, cy)
fmt.Fprintf(f, " cylinder(d=cell_slot, h=bay_h);\n\n")
}
}
writeRoutingHoles(f, lay, cfg)
fmt.Fprintf(f, "}\n")
}
func writeRoutingHoles(f io.Writer, lay BatteryLayout, cfg *BatteryConfig) {
wall := lay.Wall
for _, rh := range cfg.RoutingHoles {
d := rh.Diameter
if d <= 0 {
d = 3.0
}
pos := rh.Position
if pos < 0 {
pos = 0
}
if pos > 1 {
pos = 1
}
ht := rh.Height
if ht < 0 {
ht = 0
}
if ht > 1 {
ht = 1
}
switch rh.Face {
case "front":
cx := wall + pos*(lay.BayWidth-2*wall)
cz := wall + ht*(lay.BayHeight-2*wall)
fmt.Fprintf(f, " // Routing hole (front wall)\n")
fmt.Fprintf(f, " translate([%f, -0.1, %f])\n", cx, cz)
fmt.Fprintf(f, " rotate([-90, 0, 0])\n")
fmt.Fprintf(f, " cylinder(d=%f, h=%f);\n\n", d, wall+0.2)
case "back":
cx := wall + pos*(lay.BayWidth-2*wall)
cz := wall + ht*(lay.BayHeight-2*wall)
fmt.Fprintf(f, " // Routing hole (back wall)\n")
fmt.Fprintf(f, " translate([%f, %f, %f])\n", cx, lay.BayDepth-wall-0.1, cz)
fmt.Fprintf(f, " rotate([-90, 0, 0])\n")
fmt.Fprintf(f, " cylinder(d=%f, h=%f);\n\n", d, wall+0.2)
case "left":
cy := wall + pos*(lay.BayDepth-2*wall)
cz := wall + ht*(lay.BayHeight-2*wall)
fmt.Fprintf(f, " // Routing hole (left wall)\n")
fmt.Fprintf(f, " translate([-0.1, %f, %f])\n", cy, cz)
fmt.Fprintf(f, " rotate([0, 90, 0])\n")
fmt.Fprintf(f, " cylinder(d=%f, h=%f);\n\n", d, wall+0.2)
case "right":
cy := wall + pos*(lay.BayDepth-2*wall)
cz := wall + ht*(lay.BayHeight-2*wall)
fmt.Fprintf(f, " // Routing hole (right wall)\n")
fmt.Fprintf(f, " translate([%f, %f, %f])\n", lay.BayWidth-wall-0.1, cy, cz)
fmt.Fprintf(f, " rotate([0, 90, 0])\n")
fmt.Fprintf(f, " cylinder(d=%f, h=%f);\n\n", d, wall+0.2)
case "floor":
cx := wall + pos*(lay.BayWidth-2*wall)
cy := wall + ht*(lay.BayDepth-2*wall)
fmt.Fprintf(f, " // Routing hole (floor)\n")
fmt.Fprintf(f, " translate([%f, %f, -0.1])\n", cx, cy)
fmt.Fprintf(f, " cylinder(d=%f, h=%f);\n\n", d, wall+0.2)
}
}
}
// WriteBatterySeparatorSCAD writes the egg-carton separator piece.
func WriteBatterySeparatorSCAD(filename string, cfg *BatteryConfig) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return writeBatterySeparatorSCADTo(f, cfg)
}
// GenerateBatterySeparatorSCADString returns the separator SCAD as a string.
func GenerateBatterySeparatorSCADString(cfg *BatteryConfig) (string, error) {
var buf bytes.Buffer
if err := writeBatterySeparatorSCADTo(&buf, cfg); err != nil {
return "", err
}
return buf.String(), nil
}
func writeBatterySeparatorSCADTo(f io.Writer, cfg *BatteryConfig) error {
lay := cfg.ComputeLayout()
wall := lay.Wall
clr := lay.Clr
cellSlot := lay.CellDia + 2*clr
if cfg.Orientation == "vertical" {
return writeVerticalSeparator(f, lay, cellSlot, wall, clr)
}
return writeHorizontalSeparator(f, lay, cellSlot, wall, clr)
}
func writeHorizontalSeparator(f io.Writer, lay BatteryLayout, cellSlot, wall, clr float64) error {
// The separator mirrors the bay base: same external dimensions, with
// egg-carton bumps that capture the top half of each cell.
// Total height = same as bay so it registers properly.
cellCavityLen := lay.CellLen + 2*clr
fmt.Fprintf(f, "// Battery separator (egg-carton retainer) — horizontal cells\n")
fmt.Fprintf(f, "$fn = 48;\n\n")
fmt.Fprintf(f, "cell_slot = %f;\n", cellSlot)
fmt.Fprintf(f, "wall = %f;\n", wall)
fmt.Fprintf(f, "bay_w = %f;\n", lay.BayWidth)
fmt.Fprintf(f, "bay_d = %f;\n", lay.BayDepth)
fmt.Fprintf(f, "bay_h = %f;\n\n", lay.BayHeight)
fmt.Fprintf(f, "difference() {\n")
fmt.Fprintf(f, " cube([bay_w, bay_d, bay_h]);\n\n")
for row := 0; row < lay.Rows; row++ {
for col := 0; col < lay.Cols; col++ {
cy := wall + cellSlot/2 + float64(col)*(cellSlot+wall)
cz := wall + cellSlot/2 + float64(row)*(cellSlot+wall)
fmt.Fprintf(f, " // Cell [%d,%d]\n", row, col)
// Cell cylinder — same position as bay
fmt.Fprintf(f, " translate([wall, %f, %f])\n", cy, cz)
fmt.Fprintf(f, " rotate([0, 90, 0])\n")
fmt.Fprintf(f, " cylinder(d=cell_slot, h=%f);\n", cellCavityLen)
// Cut below cell center (keep top half = retainer bump)
fmt.Fprintf(f, " translate([wall, %f, -0.1])\n", cy-cellSlot/2)
fmt.Fprintf(f, " cube([%f, cell_slot, %f]);\n\n", cellCavityLen, cz+0.1)
}
}
fmt.Fprintf(f, "}\n")
return nil
}
func writeVerticalSeparator(f io.Writer, lay BatteryLayout, cellSlot, wall, clr float64) error {
// For vertical cells, the separator is a plate with circular holes
// that sit over the cells, providing lateral retention.
plateH := wall
lipDepth := cellSlot * 0.3 // how far the lip extends down into the cell socket
sepH := plateH + lipDepth
fmt.Fprintf(f, "// Battery separator (retainer plate) — vertical cells\n")
fmt.Fprintf(f, "$fn = 48;\n\n")
fmt.Fprintf(f, "difference() {\n")
fmt.Fprintf(f, " cube([%f, %f, %f]);\n\n", lay.BayWidth, lay.BayDepth, sepH)
for row := 0; row < lay.Rows; row++ {
for col := 0; col < lay.Cols; col++ {
cx := wall + cellSlot/2 + float64(col)*(cellSlot+wall)
cy := wall + cellSlot/2 + float64(row)*(cellSlot+wall)
// Hole slightly smaller than cell so it retains but allows insertion
retainDia := lay.CellDia - 1.0
if retainDia < lay.CellDia*0.7 {
retainDia = lay.CellDia * 0.7
}
fmt.Fprintf(f, " // Cell [%d,%d] retention hole\n", row, col)
fmt.Fprintf(f, " translate([%f, %f, -0.1])\n", cx, cy)
fmt.Fprintf(f, " cylinder(d=%f, h=%f);\n\n", retainDia, sepH+0.2)
}
}
fmt.Fprintf(f, "}\n")
return nil
}
// WriteBatteryLidSCAD writes the clip-on lid for the battery bay.
func WriteBatteryLidSCAD(filename string, cfg *BatteryConfig) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return writeBatteryLidSCADTo(f, cfg)
}
// GenerateBatteryLidSCADString returns the lid SCAD as a string.
func GenerateBatteryLidSCADString(cfg *BatteryConfig) (string, error) {
var buf bytes.Buffer
if err := writeBatteryLidSCADTo(&buf, cfg); err != nil {
return "", err
}
return buf.String(), nil
}
func writeBatteryLidSCADTo(f io.Writer, cfg *BatteryConfig) error {
lay := cfg.ComputeLayout()
wall := lay.Wall
lipH := 2.0 // how far the lid lip extends down into the bay
lidPlate := wall // top plate thickness
clipTol := 0.15 // tolerance for clip fit
fmt.Fprintf(f, "// Battery bay lid — clips onto bay walls\n")
fmt.Fprintf(f, "$fn = 48;\n\n")
outerW := lay.BayWidth + 2*clipTol
outerD := lay.BayDepth + 2*clipTol
innerW := lay.BayWidth - 2*wall + 2*clipTol
innerD := lay.BayDepth - 2*wall + 2*clipTol
fmt.Fprintf(f, "difference() {\n")
fmt.Fprintf(f, " cube([%f, %f, %f]);\n", outerW, outerD, lipH+lidPlate)
fmt.Fprintf(f, " translate([%f, %f, -0.1])\n", (outerW-innerW)/2, (outerD-innerD)/2)
fmt.Fprintf(f, " cube([%f, %f, %f]);\n", innerW, innerD, lipH+0.1)
fmt.Fprintf(f, "}\n")
return nil
}
// GenerateBatteryOutputs generates all battery SCAD files for the given config.
func GenerateBatteryOutputs(cfg *BatteryConfig, outputDir string) ([]string, error) {
if cfg == nil {
return nil, fmt.Errorf("no battery config")
}
os.MkdirAll(outputDir, 0755)
var files []string
bayPath := filepath.Join(outputDir, "battery_bay.scad")
if err := WriteBatteryBaySCAD(bayPath, cfg); err != nil {
return nil, fmt.Errorf("write battery bay: %v", err)
}
files = append(files, bayPath)
if cfg.HasLid {
lidPath := filepath.Join(outputDir, "battery_lid.scad")
if err := WriteBatteryLidSCAD(lidPath, cfg); err != nil {
return nil, fmt.Errorf("write battery lid: %v", err)
}
files = append(files, lidPath)
} else {
sepPath := filepath.Join(outputDir, "battery_separator.scad")
if err := WriteBatterySeparatorSCAD(sepPath, cfg); err != nil {
return nil, fmt.Errorf("write battery separator: %v", err)
}
files = append(files, sepPath)
}
return files, nil
}