main
noah amfm-receiver-molecule: KiCad project, fab files, 3D and README (6/7) 53707c4 1d ago
"""JLCPCB assembly files: BOM (Comment/Designator/Footprint/LCSC Part #) and CPL (pick & place).

CPL rotations are converted from KiCad's footprint orientation to JLCPCB's (EasyEDA) orientation
with the JLCKicadTools correction table (matthewlai/JLCKicadTools, cpl_rotations_db.csv) plus a
rule for the EC11 encoder.  Positions are the footprint origin (the body centre for SMD parts), or the pad
centre for through-hole parts whose origin is pin 1, measured from the board's drill/place origin
(MP1, the molecule's origin machine pin) (the drill/place origin used for the Gerbers), +Y up.
Always check the placement preview on the JLCPCB order page before paying.
"""
import csv, os, re, sys
from collections import OrderedDict
import pcbnew
from design import DESIGN

HERE = os.path.dirname(os.path.abspath(__file__))
PROJ = os.path.abspath(os.path.join(HERE, '..'))
FAB = os.path.join(PROJ, 'fab')
BOARD = os.path.join(PROJ, 'amfm_receiver.kicad_pcb')
ORIGIN = None                  # taken from the board's drill/place (aux) origin = MP1

# (footprint-name regex, rotation correction in degrees) - first match wins
ROTATION_DB = [
    (r'^SOT-223', 180), (r'^SOT-23-5', -90), (r'^SOT-23-6', -90), (r'^SOT-23', -90), (r'^SOT-89', 180),
    (r'^SOT-353', 180), (r'^SOT-363', 180), (r'^LQFP-', 270), (r'^TQFP-', 270), (r'^SOIC-', 270),
    (r'^TSSOP-', 270), (r'^QFN-', 270), (r'^USB_C_Receptacle_HRO_TYPE-C-31-M-12', 180),
    (r'^PinHeader_2x05_P1\.27mm_Vertical', 90),
    (r'^RotaryEncoder_Alps_EC11E', -90),     # KiCad Alps land pattern vs LCSC EC11 pattern
]


def correction(fpname):
    for pat, rot in ROTATION_DB:
        if re.search(pat, fpname):
            return rot
    return 0


def main():
    os.makedirs(FAB, exist_ok=True)
    parts = {p.ref: p for p in DESIGN.parts}
    board = pcbnew.LoadBoard(BOARD)
    ao = board.GetDesignSettings().GetAuxOrigin()
    origin = (pcbnew.ToMM(ao.x), pcbnew.ToMM(ao.y))
    rows = []
    for fp in board.GetFootprints():
        ref = fp.GetReference()
        p = parts.get(ref)
        if p is None or not p.in_bom or 'LCSC' not in p.fields:
            continue                    # Adom machine pins/contacts are pressed in by the Adom Factory
        pads = [q for q in fp.Pads() if q.GetAttribute() != pcbnew.PAD_ATTRIB_NPTH]
        fpname0 = fp.GetFPID().GetLibItemName().wx_str()
        pin1_origin = re.search(r'^(PinHeader_|RotaryEncoder_)', fpname0) is not None
        if pads and pin1_origin:            # origin at pin 1: use the centre of the pads
            xs = [pcbnew.ToMM(q.GetPosition().x) for q in pads]
            ys = [pcbnew.ToMM(q.GetPosition().y) for q in pads]
            cx, cy = (min(xs) + max(xs)) / 2, (min(ys) + max(ys)) / 2
        else:
            pos = fp.GetPosition()
            cx, cy = pcbnew.ToMM(pos.x), pcbnew.ToMM(pos.y)
        fpname = fp.GetFPID().GetLibItemName().wx_str()
        rot = (fp.GetOrientationDegrees() + correction(fpname)) % 360
        rows.append((ref, cx - origin[0], origin[1] - cy, 'Top' if fp.GetLayer() == pcbnew.F_Cu else 'Bottom',
                     rot, fpname))

    def natural(r):
        m = re.match(r'([A-Z]+)(\d+)', r[0])
        return (m.group(1), int(m.group(2)))
    rows.sort(key=natural)
    with open(os.path.join(FAB, 'amfm_receiver_CPL_JLCPCB.csv'), 'w', newline='') as f:
        w = csv.writer(f)
        w.writerow(['Designator', 'Mid X', 'Mid Y', 'Layer', 'Rotation'])
        for ref, x, y, layer, rot, fpname in rows:
            w.writerow([ref, '%.3fmm' % x, '%.3fmm' % y, layer, '%g' % round(rot, 3)])
    # ---------------------------------------------------------------- BOM grouped by LCSC part
    groups = OrderedDict()
    for p in sorted(DESIGN.parts, key=lambda p: natural((p.ref,))):
        if not p.in_bom or 'LCSC' not in p.fields:
            continue
        code = p.fields['LCSC']
        g = groups.setdefault(code, dict(refs=[], parts=[]))
        g['refs'].append(p.ref)
        g['parts'].append(p)
    with open(os.path.join(FAB, 'amfm_receiver_BOM_JLCPCB.csv'), 'w', newline='') as f:
        w = csv.writer(f)
        w.writerow(['Comment', 'Designator', 'Footprint', 'LCSC Part #'])
        for code, g in groups.items():
            p = g['parts'][0]
            comment = p.fields.get('MPN') or p.value
            w.writerow([comment, ','.join(g['refs']), p.footprint.split(':')[1], code])
    tht = [r for r in rows if re.search(r'EC11|PinHeader|BWSMA', r[5])]
    print('CPL: %d placements (%d through-hole: %s)' % (len(rows), len(tht), ' '.join(r[0] for r in tht)))
    print('BOM: %d JLC line items, %d parts' % (len(groups), sum(len(g['refs']) for g in groups.values())))
    return groups


if __name__ == '__main__':
    main()