main
noah amfm-receiver-molecule: KiCad project, fab files, 3D and README (7/7) ef18fb3 1d ago
"""Pick a JLCPCB (LCSC) part for every passive BOM group, with value/package/dielectric checks."""
import json, os, re, sys
from collections import OrderedDict
from design import DESIGN
import jlc

OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'build', 'jlc_passives.json')

# capacitors that must be C0G/NP0 even above 10 nF (none) / may be X7R even below (coupling / decoupling)
C0G_MAX = 10e-9          # every capacitor <= 10 nF is C0G/NP0 unless listed below
X7R_OK_SMALL = {'10n'}   # 10 nF parts are coupling/bypass: X7R fine


def val_to_float(v, unit):
    m = re.match(r'^([\d.]+)([pnumkM]?)$', v)
    mult = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, '': 1.0}
    return float(m.group(1)) * mult[m.group(2)]


def jlc_value_text(v, unit):
    """Design value ('4.7k', '100n') -> JLC attribute text ('4.7kΩ', '100nF')."""
    if unit == 'Ω':
        return v + 'Ω'
    if unit == 'F':
        return v.replace('u', 'u') + 'F'
    if unit == 'H':
        return v + 'H'
    return v


def attr(p, *names):
    for n in names:
        if n in p['attrs']:
            return p['attrs'][n]
    return None


def voltage(p):
    v = attr(p, 'Voltage Rating', 'Rated Voltage')
    m = re.search(r'([\d.]+)\s*V', v or p['describe'])
    return float(m.group(1)) if m else 0.0


def rank(p):
    return (p['type'] == 'base', p['preferred'], p['stock'])


def pick_resistor(value, pkg='0603'):
    want = jlc_value_text(value, 'Ω')
    cands = []
    for q in ('%s %s' % (want, pkg), '%s %s resistor 1%%' % (want, pkg)):
        for p in jlc.search(q, size=50):
            if p['package'] != pkg or 'Resistor' not in (p['category'] or ''):
                continue
            if attr(p, 'Resistance') != want or p['stock'] < 500:
                continue
            if '±1%' not in p['describe']:
                continue
            cands.append(p)
    return sorted({c['code']: c for c in cands}.values(), key=rank, reverse=True)


def pick_capacitor(value, pkg, need_c0g, vmin):
    want = jlc_value_text(value, 'F')
    cands = []
    for q in ('%s %s' % (want, pkg), '%s %s %s' % (want, pkg, 'C0G' if need_c0g else 'X7R')):
        for p in jlc.search(q, size=50):
            if p['package'] != pkg or 'Capacitor' not in (p['category'] or ''):
                continue
            if attr(p, 'Capacitance') != want or p['stock'] < 500:
                continue
            d = p['describe']
            diel = 'C0G' in d or 'NP0' in d or 'NPO' in d
            if need_c0g and not diel:
                continue
            if not need_c0g and not (diel or 'X7R' in d or 'X5R' in d):
                continue
            if voltage(p) < vmin:
                continue
            cands.append(p)
    return sorted({c['code']: c for c in cands}.values(), key=rank, reverse=True)


def pick_inductor(value, pkg, wirewound):
    want = jlc_value_text(value, 'H')
    cands = []
    for q in ('%s %s' % (want, pkg), '%s %s inductor' % (want, pkg)):
        for p in jlc.search(q, size=50):
            if p['package'] != pkg or 'nductor' not in (p['category'] or '') + p['describe']:
                continue
            if attr(p, 'Inductance') != want and (' ' + want + ' ') not in (' ' + p['describe'] + ' '):
                continue
            if p['stock'] < 100:
                continue
            if wirewound and not ('LQW' in (p['mpn'] or '') or 'ire' in p['describe'] or 'CS' in (p['mpn'] or '')):
                continue
            cands.append(p)
    return sorted({c['code']: c for c in cands}.values(), key=rank, reverse=True)


def groups():
    g = OrderedDict()
    for p in DESIGN.parts:
        if not p.in_bom:
            continue
        g.setdefault((p.lib_id, p.value, p.footprint), []).append(p)
    return g


def main():
    res = OrderedDict()
    for (lib, value, fp), parts in groups().items():
        refs = [p.ref for p in parts]
        pkg = re.search(r'_(\d{4})_', fp)
        pkg = pkg.group(1) if pkg else None
        notes = ' '.join(p.description for p in parts)
        if lib == 'Device:R':
            c = pick_resistor(value, pkg)
        elif lib == 'Device:C':
            f = val_to_float(value, 'F')
            need_c0g = f <= C0G_MAX and value not in X7R_OK_SMALL
            vmin = 16 if f < 4e-6 else 10
            c = pick_capacitor(value, pkg, need_c0g, vmin)
        elif lib == 'Device:L':
            c = pick_inductor(value, pkg, wirewound=pkg == '0603')
        else:
            continue
        key = '%s|%s|%s' % (lib, value, fp)
        res[key] = dict(refs=refs, candidates=c[:4])
        best = c[0] if c else None
        print('%-8s %-6s %-5s x%-2d -> %s' % (lib.split(':')[1], value, pkg, len(refs),
                                             jlc.fmt(best) if best else 'NONE FOUND'))
    json.dump(res, open(OUT, 'w'), indent=1)


if __name__ == '__main__':
    main()