main
TylerK Replace discrete-analog board with MCU/IC redesign: STM32G031 P&O MPPT + CC/CV firmware, LM5175 buck-boost, INA180 curr… e48ba30 1mo ago
#!/usr/bin/env python3
"""Parse the kicad-cli 'kicadsexpr' netlist export into a simple JSON:
components: {ref: {value, footprint}}
ref_pin_nets: {ref: {pin: net_name}}
Uses a balanced-paren block splitter (not fragile line-based regex) since
the export's indentation/line-wrapping isn't uniform."""
import re, json, sys


def split_blocks(text, tag):
    """Every top-level '(tag ...)' block within text, balanced-paren aware."""
    out = []
    i = 0
    n = len(text)
    needle = '(' + tag
    while True:
        i = text.find(needle, i)
        if i == -1:
            break
        # must be a real token boundary (not part of a longer word)
        j = i + len(needle)
        if j < n and (text[j].isalnum() or text[j] == '_'):
            i = j
            continue
        depth = 0
        k = i
        ins = False
        esc = False
        while k < n:
            ch = text[k]
            if esc:
                esc = False
            elif ch == '\\':
                esc = True
            elif ch == '"':
                ins = not ins
            elif not ins:
                if ch == '(':
                    depth += 1
                elif ch == ')':
                    depth -= 1
                    if depth == 0:
                        k += 1
                        break
            k += 1
        out.append(text[i:k])
        i = k
    return out


def field(block, name, quoted=True):
    pat = r'\(' + name + (r' "([^"]*)"\)' if quoted else r' ([^\s\)]+)\)')
    m = re.search(pat, block)
    return m.group(1) if m else None


def parse(path):
    txt = open(path, errors='replace').read()

    comp_start = txt.find('(components')
    comp_end = txt.find('(libparts')
    comp_section = txt[comp_start:comp_end]
    components = {}
    for block in split_blocks(comp_section, 'comp'):
        ref = field(block, 'ref')
        if not ref:
            continue
        components[ref] = {
            'value': field(block, 'value') or '',
            'footprint': field(block, 'footprint') or '',
        }

    nets_start = txt.find('(nets')
    nets_section = txt[nets_start:]
    ref_pin_nets = {}
    for net_block in split_blocks(nets_section, 'net'):
        name = field(net_block, 'name')
        if name is None:
            continue
        for node_block in split_blocks(net_block, 'node'):
            ref = field(node_block, 'ref')
            pin = field(node_block, 'pin')
            if ref and pin:
                ref_pin_nets.setdefault(ref, {})[pin] = name

    return {'components': components, 'ref_pin_nets': ref_pin_nets}


if __name__ == '__main__':
    src = sys.argv[1] if len(sys.argv) > 1 else 'mppt-mcu-netlist.net'
    out = sys.argv[2] if len(sys.argv) > 2 else 'mppt_mcu_netlist.json'
    data = parse(src)
    json.dump(data, open(out, 'w'), indent=1)
    print(f"components: {len(data['components'])}  refs-with-nets: {len(data['ref_pin_nets'])}")