main
John Lauer R0 engineering checkpoint: checked schematic, sourcing, servo evidence and safety-policy code; PCB and qualification incomplete 10054d3 1h ago
"""Minimal KiCad s-expression reader/writer (enough to lift symbols out of a library and write a schematic)."""
import re

_TOK = re.compile(r'\s*(?:(\()|(\))|("(?:[^"\\]|\\.)*")|([^\s()"]+))', re.S)


class Sym(str):
    """An unquoted atom (keyword, number)."""


def parse(text):
    stack, cur = [], []
    for m in _TOK.finditer(text):
        lp, rp, qs, atom = m.groups()
        if lp:
            stack.append(cur); cur = []
        elif rp:
            done = cur; cur = stack.pop(); cur.append(done)
        elif qs is not None:
            cur.append(qs[1:-1].replace('\\"', '"').replace("\\\\", "\\"))
        elif atom is not None:
            cur.append(Sym(atom))
    assert not stack
    return cur[0] if len(cur) == 1 else cur


def q(s):
    s = str(s).replace(chr(10), "__ADOM_NEWLINE__")
    return '"' + str(s).replace("\\", "\\\\").replace('"', '\\"').replace("__ADOM_NEWLINE__", "\\n") + '"'


def dump(x, ind=0):
    if isinstance(x, list):
        if not x:
            return "()"
        head = dump(x[0])
        simple = all(not isinstance(e, list) for e in x)
        if simple:
            return "(" + " ".join(dump(e) for e in x) + ")"
        pad = "\t" * (ind + 1)
        parts = [head] + [e for e in x[1:]]
        out = "(" + head
        for e in x[1:]:
            out += ("\n" + pad + dump(e, ind + 1)) if isinstance(e, list) else (" " + dump(e))
        return out + "\n" + "\t" * ind + ")"
    if isinstance(x, Sym):
        return str(x)
    if isinstance(x, (int, float)):
        return f"{x:g}" if isinstance(x, float) else str(x)
    return q(x)


def find(node, key):
    return [e for e in node if isinstance(e, list) and e and e[0] == key]


def first(node, key):
    r = find(node, key)
    return r[0] if r else None


def lib_symbols(path):
    lib = parse(open(path, encoding="utf-8").read())
    return {s[1]: s for s in find(lib, "symbol")}


def flatten(name, syms, libname):
    """Return the symbol `name` as a standalone lib_symbols entry named libname:name (extends resolved)."""
    import copy
    s = copy.deepcopy(syms[name])
    ext = first(s, "extends")
    if ext is not None:
        base = copy.deepcopy(syms[ext[1]])
        props = {p[1]: p for p in find(s, "property")}
        body = [e for e in base[2:] if not (isinstance(e, list) and e and e[0] == "property")]
        bprops = [props.get(p[1], p) for p in find(base, "property")]
        for p in find(s, "property"):
            if p[1] not in {bp[1] for bp in bprops}:
                bprops.append(p)
        s = [Sym("symbol"), name] + [e for e in s[2:] if isinstance(e, list) and e[0] not in ("extends", "property")] + bprops + body
        # rename unit sub-symbols base_N_M -> name_N_M
        for e in s:
            if isinstance(e, list) and e and e[0] == "symbol" and isinstance(e[1], str) and e[1].startswith(ext[1] + "_"):
                e[1] = name + e[1][len(ext[1]):]
    s[1] = f"{libname}:{name}"
    return s


def pins(sym):
    """[(number, name, x, y, angle)] across all units of a flattened symbol."""
    out = []
    for u in find(sym, "symbol"):
        for p in find(u, "pin"):
            at = first(p, "at"); nm = first(p, "name"); no = first(p, "number")
            out.append((no[1], nm[1], float(at[1]), float(at[2]), float(at[3]) if len(at) > 3 else 0.0))
    return out