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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
"""Read KiCad .kicad_sym libraries: extract symbol blocks, flatten `extends`, expose pin geometry."""
import re, os
class SymLib:
def __init__(self, paths):
self.raw = {} # (lib, name) -> block text
self.libs = {}
for p in paths:
lib = os.path.basename(p).replace('.kicad_sym', '')
txt = open(p, errors='replace').read()
self.libs[lib] = txt
for m in re.finditer(r'^\t\(symbol "([^"]+)"\n(.*?)^\t\)$', txt, re.M | re.S):
self.raw[(lib, m.group(1))] = m.group(0)
def block(self, lib, name):
return self.raw.get((lib, name))
def _extends(self, blk):
m = re.search(r'^\t\t\(extends "([^"]+)"\)', blk, re.M)
return m.group(1) if m else None
def resolved(self, lib, name):
"""Return (block_text_with_children, effective_child_basename)."""
blk = self.block(lib, name)
if blk is None:
raise KeyError(f"{lib}:{name}")
par = self._extends(blk)
if not par:
return blk, name
pblk, pbase = self.resolved(lib, par)
return pblk, pbase
def pins(self, lib, name):
"""{unit: [(number, pinname, etype, x, y, angle)]} using the resolved (parent) geometry.
Unit 0 in KiCad's convention is pins COMMON TO ALL UNITS (e.g. a timer's
shared GND/VCC), not an independently-placeable unit on its own - it is
merged into every other real unit here, matching how eeschema treats it.
Placing "unit 0" as its own instance (as if it were a normal unit) produces
a symbol KiCad does not recognize as electrically part of the same part.
"""
blk, base = self.resolved(lib, name)
out = {}
for um in re.finditer(r'\t\t\(symbol "' + re.escape(base) + r'_(\d+)_(\d+)"\n(.*?)(?=\n\t\t\(symbol "|\Z)',
blk, re.S):
unit, style, body = int(um.group(1)), int(um.group(2)), um.group(3)
for pm in re.finditer(
r'\(pin (\w+) \w+\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)\s*\n\s*\(length [\d.]+\)(.*?)\(number "([^"]+)"',
body, re.S):
etype, x, y, ang, mid, num = pm.groups()
nm = re.search(r'\(name "([^"]*)"', mid)
out.setdefault(unit, [])
if not any(p[0] == num for p in out[unit]):
out[unit].append((num, nm.group(1) if nm else '', etype,
float(x), float(y), int(ang)))
if 0 in out:
common = out.pop(0)
if out:
for u in out:
for p in common:
if not any(q[0] == p[0] for q in out[u]):
out[u].append(p)
else:
# symbol has ONLY common pins (e.g. PWR_FLAG) - unit 0 IS the part
out[1] = common
return out
@staticmethod
def _forms(body):
"""Split a symbol body into its top-level (...) forms, in order."""
out, i, n = [], 0, len(body)
while i < n:
if body[i] != '(':
i += 1; continue
d, j, ins, esc = 0, i, False, False
while j < n:
ch = body[j]
if esc: esc = False
elif ch == '\\': esc = True
elif ch == '"': ins = not ins
elif not ins:
if ch == '(': d += 1
elif ch == ')':
d -= 1
if d == 0: j += 1; break
j += 1
out.append(body[i:j]); i = j
return out
def lib_symbol_entry(self, lib, name):
"""The (symbol "lib:name" ...) text to embed in a schematic's lib_symbols.
For a derived symbol (extends), compose the parent's geometry/settings with
the CHILD's own properties, which is how eeschema resolves it.
"""
blk = self.block(lib, name)
body = blk.split('\n', 1)[1].rsplit('\t)', 1)[0]
par = self._extends(blk)
if not par:
return f'\t(symbol "{lib}:{name}"\n{body}\t)'
pblk, pbase = self.resolved(lib, par)
pbody = pblk.split('\n', 1)[1].rsplit('\t)', 1)[0]
kid = {}
for f in self._forms(body):
m = re.match(r'\(property "([^"]+)"', f)
if m: kid[m.group(1)] = f
head, units, tail = [], [], []
for f in self._forms(pbody):
k = re.match(r'\((\w+)', f).group(1)
if k == 'symbol':
units.append(f.replace(f'"{pbase}_', f'"{name}_'))
elif k == 'property':
nm = re.match(r'\(property "([^"]+)"', f).group(1)
if nm in kid:
head.append(kid.pop(nm))
else:
head.append(f)
elif k == 'embedded_fonts':
tail.append(f)
else:
head.append(f)
head.extend(kid.values())
parts = head + units + tail
return f'\t(symbol "{lib}:{name}"\n' + '\n'.join('\t\t' + x.replace('\n\t\t', '\n\t\t') for x in parts) + '\n\t)'