board
Solar MPPT Power System
Public Unreviewedby TylerK
A 30 W solar battery charger with regulated 5 V and 3.3 V rails: an STM32G031 running perturb & observe MPPT and a CC/CV charge state machine, steering a TI LM5175 4-switch buck-boost through a single analog control input. Four-layer KiCad board, schematic and PCB generated from Python, with an electrothermal simulation run against the published board file.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
#!/usr/bin/env python3
"""Merge collinear runs of track segments into single segments.
The fine-grid hand-router works on a 0.025mm raster and emits one segment per
cell step, so a single straight 40mm trace can arrive as 1600 segments. That is
electrically identical to one segment and KiCad renders it identically, but it
is not harmless:
* the electrothermal model rasterises each net's copper by unioning polygons,
and a chain of thousands of 0.025mm slivers does not survive that cleanly --
it reported five terminals as disconnected on copper that both KiCad's
connectivity and an exact-geometry checker call one continuous piece;
* the board file gets large, and every tool that parses it gets slower.
Merging is safe because it only ever replaces a chain of collinear, same-net,
same-width, same-layer segments joined end to end with the single segment they
already describe. Nothing moves, no width changes, and a branch touching the
middle of the chain still touches the merged segment at the same point.
"""
import math, re, sys
from collections import defaultdict
PCB = ("/mnt/c/Users/kirkt/Documents/mppt-power-system-mcu/"
"mppt-power-system-mcu.kicad_pcb")
TOL = 1e-6
def main(path=PCB):
text = open(path, errors="replace").read()
# Segment blocks come from several writers and do not all order their
# fields the same way, so pull each field out of the block individually
# rather than demanding one layout. A single rigid pattern matched only
# 2,625 of 7,240 segments and silently left the rest unmerged.
blk_pat = re.compile(r'\t\(segment\n(?:\t\t\([^\n]*\)\n)+\t\)\n')
segs, spans = [], []
for m in blk_pat.finditer(text):
blk = m.group(0)
s = re.search(r'\(start ([-\d.]+) ([-\d.]+)\)', blk)
e = re.search(r'\(end ([-\d.]+) ([-\d.]+)\)', blk)
w = re.search(r'\(width ([\d.]+)\)', blk)
L = re.search(r'\(layer "([^"]+)"\)', blk)
n = re.search(r'\(net (?:\d+\s+)?"?([^")\n]*)"?\)', blk)
if not (s and e and w and L):
continue
segs.append([float(s.group(1)), float(s.group(2)),
float(e.group(1)), float(e.group(2)),
w.group(1), L.group(1), (n.group(1) if n else "")])
spans.append((m.start(), m.end()))
if not segs:
print("no segments matched -- file left alone")
return 0
# group by (net, layer, width); within a group chain by shared endpoint
groups = defaultdict(list)
for i, s in enumerate(segs):
groups[(s[6], s[5], s[4])].append(i)
drop = set()
replace = {}
for key, idxs in groups.items():
ends = defaultdict(list)
for i in idxs:
ax, ay, bx, by = segs[i][:4]
ends[(round(ax, 4), round(ay, 4))].append(i)
ends[(round(bx, 4), round(by, 4))].append(i)
used = set()
for i in idxs:
if i in used:
continue
ax, ay, bx, by = segs[i][:4]
chain = [i]; used.add(i)
# extend from both ends while the next segment is collinear
for which in (0, 1):
cx, cy = (ax, ay) if which == 0 else (bx, by)
px, py = (bx, by) if which == 0 else (ax, ay)
while True:
cand = [j for j in ends.get((round(cx, 4), round(cy, 4)), [])
if j not in used]
nxt = None
for j in cand:
jx0, jy0, jx1, jy1 = segs[j][:4]
if abs(jx0 - cx) < TOL and abs(jy0 - cy) < TOL:
ox, oy = jx1, jy1
elif abs(jx1 - cx) < TOL and abs(jy1 - cy) < TOL:
ox, oy = jx0, jy0
else:
continue
# collinear with the direction we arrived from?
v1x, v1y = cx - px, cy - py
v2x, v2y = ox - cx, oy - cy
if abs(v1x * v2y - v1y * v2x) > 1e-7:
continue
if v1x * v2x + v1y * v2y <= 0: # doubles back
continue
nxt = (j, ox, oy); break
if not nxt:
break
j, ox, oy = nxt
used.add(j); chain.append(j)
px, py = cx, cy
cx, cy = ox, oy
if which == 0:
ax, ay = cx, cy
else:
bx, by = cx, cy
if len(chain) > 1:
replace[chain[0]] = (ax, ay, bx, by)
drop.update(chain[1:])
if not drop:
print("nothing to merge")
return 0
out, pos = [], 0
for i, (s0, s1) in enumerate(spans):
out.append(text[pos:s0])
if i in drop:
pass
elif i in replace:
ax, ay, bx, by = replace[i]
blk = text[s0:s1]
blk = re.sub(r'\(start [-\d.]+ [-\d.]+\)', f'(start {ax:g} {ay:g})', blk, 1)
blk = re.sub(r'\(end [-\d.]+ [-\d.]+\)', f'(end {bx:g} {by:g})', blk, 1)
out.append(blk)
else:
out.append(text[s0:s1])
pos = s1
out.append(text[pos:])
new = "".join(out)
with open(path, "w") as fh:
fh.write(new)
print(f"merged {len(drop)} segment(s) away: {len(segs)} -> {len(segs)-len(drop)}")
return 0
if __name__ == "__main__":
sys.exit(main())