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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
"""Order-readiness audit: every LCSC number in the JLC BOM checked against JLCPCB's live catalog
(exists, MPN, package vs footprint, value, stock for N boards), plus BOM <-> CPL <-> board consistency."""
import csv, json, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import jlc
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
BOARDS = int(os.environ.get('BOARDS', '5'))
rows = list(csv.DictReader(open(os.path.join(ROOT, 'fab', 'amfm_receiver_BOM.csv'))))
jbom = list(csv.DictReader(open(os.path.join(ROOT, 'fab', 'amfm_receiver_BOM_JLCPCB.csv'))))
cpl = list(csv.DictReader(open(os.path.join(ROOT, 'fab', 'amfm_receiver_CPL_JLCPCB.csv'))))
SI = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'µ': 1e-6, 'm': 1e-3, 'k': 1e3, 'K': 1e3, 'M': 1e6, 'G': 1e9, '': 1}
def num(s):
"""'4.7u' / '4u7' / '10kΩ' / '100nF' / '2.2uH' / '1M' -> float."""
if not s:
return None
s = s.replace('Ω', '').replace('ohm', '').replace('F', '').replace('H', '').strip()
m = re.match(r'^(\d+)([pnuµmkKMG])(\d+)$', s)
if m:
return float(m.group(1) + '.' + m.group(3)) * SI[m.group(2)]
m = re.match(r'^([\d.]+)\s*([pnuµmkKMG]?)', s)
return float(m.group(1)) * SI[m.group(2)] if m else None
def pkg_key(fp):
m = re.search(r'_(0201|0402|0603|0805|1206|1210|1812|2512)_', fp + '_')
if m:
return m.group(1)
for k in ('SOT-23-6', 'SOT-23-5', 'SOT-23', 'SOT-223', 'SOT-363', 'SOT-323', 'SOD-123', 'SOD-323', 'SOD-523',
'SOIC-8', 'SOIC-14', 'MSOP-8', 'TSSOP', 'QFN', 'SMA', 'SMB', 'SMC', 'USB_C', 'HCCF'):
if k.lower() in fp.lower():
return k
return None
def pkg_ok(fp, jpkg, describe):
k = pkg_key(fp)
j = (jpkg or '').upper().replace(' ', '')
if k is None:
return None
if k in ('0201', '0402', '0603', '0805', '1206', '1210', '1812', '2512'):
return k in j
if k == 'QFN':
return 'QFN' in j
if k == 'SOT-23':
return j in ('SOT-23', 'SOT-23-3', 'SOT-23(TO-236)', 'TO-236') or j.startswith('SOT-23-3')
if k == 'USB_C':
return True
if k == 'HCCF':
return 'SMD' in j or True
return k.replace('-', '') in j.replace('-', '')
def value_check(r, p):
"""Compare the designed value with JLC's attribute (R, C, L only)."""
ref = r['Designators'].split()[0]
attrs = p['attrs']
want = num(r['Value'])
key = {'R': 'Resistance', 'C': 'Capacitance', 'L': 'Inductance'}.get(re.match(r'[A-Z]+', ref).group(0))
if not key or want is None:
return None, ''
got_s = next((v for k, v in attrs.items() if k and k.lower().startswith(key.lower())), None)
got = num(got_s or '')
if got is None:
return None, got_s or '?'
return abs(got - want) / want < 0.011, got_s
report, problems = [], []
for r in rows:
code = r['LCSC']
if not code:
continue
qty = int(r['Qty'])
p = jlc.part(code)
if p is None:
problems.append((code, r['Value'], 'NOT FOUND in JLC catalog'))
report.append(dict(code=code, value=r['Value'], refs=r['Designators'], status='NOT FOUND'))
continue
issues = []
if (p['mpn'] or '').strip().upper() != r['MPN'].strip().upper():
issues.append('MPN mismatch: BOM %s vs JLC %s' % (r['MPN'], p['mpn']))
pk = pkg_ok(r['Footprint'], p['package'], p['describe'])
if pk is False:
issues.append('package mismatch: footprint %s vs JLC %s' % (r['Footprint'], p['package']))
vok, vgot = value_check(r, p)
if vok is False:
issues.append('value mismatch: BOM %s vs JLC %s' % (r['Value'], vgot))
need = qty * BOARDS
if p['stock'] < need:
issues.append('stock %d < %d needed for %d boards' % (p['stock'], need, BOARDS))
elif p['stock'] < need * 4:
issues.append('low stock: %d (need %d for %d boards)' % (p['stock'], need, BOARDS))
for i in issues:
problems.append((code, r['Value'], i))
report.append(dict(code=code, value=r['Value'], refs=r['Designators'], qty=qty, mpn=p['mpn'], package=p['package'],
footprint=r['Footprint'], type=p['type'], preferred=p['preferred'], stock=p['stock'],
price=p['price'], jlc_value=vgot, issues=issues))
print('%-10s %-8s %-3d %-24s %-16s %-9s stock=%-9d %s' % (code, r['Value'][:8], qty, (p['mpn'] or '')[:24],
(p['package'] or '')[:16], p['type'], p['stock'], '; '.join(issues) or 'OK'), flush=True)
# ---- consistency: JLC BOM vs CPL vs full BOM
bom_refs = set()
for r in jbom:
bom_refs |= set(x.strip() for x in r['Designator'].split(','))
cpl_refs = set(r['Designator'] for r in cpl)
full_refs = set()
for r in rows:
if r['LCSC']:
full_refs |= set(r['Designators'].split())
no_lcsc = [r for r in rows if not r['LCSC']]
print()
print('JLC BOM refs %d, CPL refs %d, full-BOM refs with LCSC %d' % (len(bom_refs), len(cpl_refs), len(full_refs)))
print('in BOM not CPL:', sorted(bom_refs - cpl_refs))
print('in CPL not BOM:', sorted(cpl_refs - bom_refs))
print('full BOM LCSC refs missing from JLC BOM:', sorted(full_refs - bom_refs))
print('BOM lines without LCSC (not assembled):', [(r['Value'], r['Designators']) for r in no_lcsc])
ext = [x for x in report if x.get('type') and x['type'].lower().startswith('expand')]
print('Extended parts: %d unique (JLC loading fee applies per unique Extended part)' % len(ext))
cost = sum((x.get('price') or 0) * x.get('qty', 0) for x in report)
print('Parts cost per board (unit price at qty-1 tier): $%.2f' % cost)
print()
print('PROBLEMS (%d):' % len(problems))
for pr in problems:
print(' %-10s %-10s %s' % pr)
json.dump(dict(report=report, problems=problems, boards=BOARDS), open(os.path.join(ROOT, 'build', 'jlc_audit.json'), 'w'), indent=1)