#!/usr/bin/env python3 """Generate a consolidated BOM from the integrated board.net. Run INSIDE the EDA container: cd hardware/eda && ./run.sh python3 ../eda/gen_bom.py Reads hardware/kicad/board.net, groups like parts, attaches MPNs, writes hardware/BOM_board.csv (authoritative part list keyed to the board.net references). """ import re, csv, os, collections ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) NET = os.path.join(ROOT, "hardware/kicad/board.net") OUT = os.path.join(ROOT, "hardware/BOM_board.csv") # value -> (manufacturer, MPN, note) MPN = { "RP2350A": ("Raspberry Pi", "RP2350A", "MCU (QFN-60); via KiCad lib symbol"), "W25Q128JVS": ("Winbond", "W25Q128JVSIQ", "16MB QSPI flash"), "PCM5102A": ("TI", "PCM5102APWR", "I2S DAC; SCK->GND (MCLK-less)"), "THAT1240": ("THAT Corp", "THAT1240S08-U", "0dB balanced line receiver; 2nd-src INA134/SSM2141"), "THAT1646": ("THAT Corp", "THAT1646S08-U", "balanced line driver, +6dB; 2nd-src DRV134/SSM2142"), "OPA1641": ("TI", "OPA1641AID", "JFET Hi-Z DI buffer"), "OPA1612": ("TI", "OPA1612AIDR", "dual: recon filter + summer"), "TPS65131": ("TI", "TPS65131RGER", "dual boost/inverter -> +/-18V"), "TPS7A4901": ("TI", "TPS7A4901DGNR", "+15V ultra-low-noise LDO; confirm Vfb"), "TPS7A3001": ("TI", "TPS7A3001DGNR", "-15V ultra-low-noise LDO; confirm Vfb"), "AP2112K-3.3": ("Diodes", "AP2112K-3.3TRG1", "3V3 LDO; confirm SOT-23-5 pinout"), "ULN2003A": ("TI", "ULN2003ADR", "shared relay driver (3 of 7 ch used)"), "RV-8803-C7": ("Micro Crystal", "RV-8803-C7", "I2C RTC; confirm footprint"), "LM393": ("TI", "LM393DR", "DNP - SIG/CLIP comparator"), "H11L1": ("Vishay", "H11L1M", "DNP - MIDI opto IN; confirm pinout"), "74LVC14": ("Nexperia", "74LVC14APW", "DNP - MIDI OUT/THRU buffer"), "PAM8302A": ("Diodes", "PAM8302AASCR", "DNP - monitor speaker amp"), "USBLC6-2SC6": ("STMicro", "USBLC6-2SC6", "USB ESD"), "TQ2SA-5V": ("Panasonic", "TQ2SA-5V", "DPDT signal relay, gold; K1 select/K2 mute/K3 gnd-lift"), "12MHz": ("Abracon", "ABM8-272-12.000MHZ-T3", "RP2350 crystal; confirm load caps"), "USB_C_Receptacle": ("GCT", "USB4085-GF-A", "USB-C; 24-pin sym vs 16-pin part - resolve at layout"), "CR2032": ("Keystone", "1066", "coin-cell holder (RTC backup)"), "MBRM120": ("onsemi", "MBRM120ET3G", "Schottky rectifier (switcher)"), "BAT54": ("onsemi", "BAT54", "Schottky (RTC diode-OR / peak-detect)"), "1N4148WS": ("onsemi", "1N4148WS", "fast diode (input clamps / MIDI protect)"), "4.7uH": ("Wurth/EPCOS", "7447789004 / B82462-G4472", "switcher inductor"), "3.3uH": ("Abracon", "AOTA-B201610S3R3-101-T", "RP2350 core SMPS inductor"), "600R": ("Murata", "BLM18KG..", "ferrite bead (USB VBUS input)"), } comps = re.findall(r'\(comp\s*\(ref "([^"]+)"\)\s*\(value "([^"]+)"\)(.*?)\(libsource', open(NET).read(), re.S) def fp(blk): m = re.search(r'\(footprint "([^"]+)"', blk); return m.group(1) if m else "" groups = collections.OrderedDict() for ref, val, blk in comps: key = (val, fp(blk)) groups.setdefault(key, []).append(ref) def sortkey(r): m = re.match(r'([A-Za-z]+)(\d+)', r); return (m.group(1), int(m.group(2))) if m else (r, 0) rows = [] for (val, foot), refs in groups.items(): refs = sorted(refs, key=sortkey) man, mpn, note = MPN.get(val, ("", "", "")) if "Potentiometer" in foot: # the level-cal trimmer (value "10k" but a pot) man, mpn, note = ("Bourns", "3296W-1-103LF", "output level cal trim (25-turn)") # heuristic DNP flag dnp = any(s in note for s in ("DNP",)) rows.append([len(refs), " ".join(refs), val, foot, man, mpn, ("DNP" if dnp else ""), note]) rows.sort(key=lambda r: (r[6] == "DNP", -r[0], r[2])) # populated first, then by qty with open(OUT, "w", newline="") as f: w = csv.writer(f) w.writerow(["Qty", "Refs", "Value", "Footprint", "Manufacturer", "MPN", "Populate", "Notes"]) w.writerows(rows) print("wrote", OUT) print("line items:", len(rows), " total placements:", sum(r[0] for r in rows))