molecule
AM/FM Receiver Molecule
Public Made by Adomby adom
USB-powered AM/FM receiver on an Adom Molecule: discrete BJT and op-amp superhet (10.7 MHz and 455 kHz IFs), STM32G0B1 tuning, self-calibration and USB audio over one USB-C cable. 96 x 96 mm pin grid, 4 layers, KiCad 10.
main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
"""Grouped BOM (CSV) with JLCPCB part data: LCSC #, MPN, Basic/Extended, live stock and unit price."""
import csv, os, re
from collections import OrderedDict
from design import DESIGN
import jlc
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.abspath(os.path.join(HERE, '..', 'fab'))
def natural(ref):
m = re.match(r'([A-Z]+)(\d+)', ref)
return (m.group(1), int(m.group(2))) if m else (ref, 0)
def main(boards=5):
os.makedirs(OUT, exist_ok=True)
groups = OrderedDict()
for p in sorted(DESIGN.parts, key=lambda p: natural(p.ref)):
if not p.in_bom:
continue
groups.setdefault(p.fields.get('LCSC') or p.fields.get('MPN'), []).append(p)
rows = []
ext = []
low_stock = []
adom = []
for code, parts in groups.items():
p = parts[0]
if 'LCSC' not in p.fields: # Adom machine pins / contacts
adom.append((code, parts))
continue
info = jlc.part(code) or {}
kind = {'base': 'Basic', 'expand': 'Extended'}.get(info.get('type'), '?')
if kind == 'Extended' and info.get('preferred'):
kind = 'Preferred Extended'
if kind == 'Extended':
ext.append(code)
need = len(parts) * boards
if info.get('stock', 0) < need * 2:
low_stock.append('%s (%s): stock %s' % (code, info.get('mpn'), info.get('stock')))
rows.append(OrderedDict([
('Qty', len(parts)),
('Designators', ' '.join(q.ref for q in parts)),
('Value', ' / '.join(sorted(set(q.value for q in parts)))),
('Footprint', p.footprint.split(':')[1]),
('Description', p.description),
('LCSC', code),
('MPN', info.get('mpn') or p.fields.get('MPN', '')),
('Manufacturer', info.get('brand') or ''),
('JLC type', kind),
('JLC stock', info.get('stock', '')),
('Unit price USD', info.get('price', '')),
]))
for mpn, parts in adom:
rows.append(OrderedDict([
('Qty', len(parts)), ('Designators', ' '.join(q.ref for q in parts)),
('Value', mpn), ('Footprint', parts[0].footprint.split(':')[1]),
('Description', 'Adom %s (press-fit by the Adom Factory)' % mpn), ('LCSC', ''),
('MPN', mpn), ('Manufacturer', 'Adom'), ('JLC type', 'Adom-supplied, not assembled by JLC'),
('JLC stock', ''), ('Unit price USD', ''),
]))
with open(os.path.join(OUT, 'amfm_receiver_BOM.csv'), 'w', newline='') as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader()
w.writerows(rows)
total = sum(r['Qty'] * (r['Unit price USD'] or 0) for r in rows)
print('Adom machine parts: %s' % ', '.join('%d x %s' % (len(ps), m) for m, ps in adom))
print('%d line items, %d placed parts, parts cost ~$%.2f per board, %d extended (setup fee) parts' % (
len(rows), sum(r['Qty'] for r in rows), total, len(ext)))
for s in low_stock:
print(' LOW STOCK for %d boards: %s' % (boards, s))
return rows
if __name__ == '__main__':
main()