AnasSarkiz/magnetic-shutter-remote-r8
A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.
- Version
- 0.4.0-prototype
- License
- unset
- Stars
- 0
scripts/write-r9-bom.py
"""Derive current R9 fitted BOM from native routing and exact dated sourcing."""
import csv
import hashlib
import json
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EVIDENCE = ROOT / 'evidence/R9-routing-2026-10-13'
def write_bom():
raw = (ROOT/'dist/index/circuit.json').read_bytes()
circuit = json.loads(raw)
if any(e['type'].endswith('_error') for e in circuit) or not any(e['type']=='pcb_trace' for e in circuit):
raise ValueError('BOM release requires a successful native routed candidate')
purchased = {p['lcsc']: p for p in json.loads((ROOT/'bom.json').read_text())['parts']}
sourced = {p['lcsc']: p for p in json.loads((EVIDENCE/'sourcing.json').read_text())['parts']}
required_fields = {'manufacturer','manufacturer_part_number','package','value','datasheet_file'}
for number, part in purchased.items():
missing_fields = sorted(field for field in required_fields if not isinstance(part.get(field),str) or not part[field].strip())
if missing_fields:
raise ValueError(f'{number}: missing required purchasing metadata {missing_fields}')
groups = {}
for component in (e for e in circuit if e['type']=='source_component'):
number, = component['supplier_part_numbers']['jlcpcb']
groups.setdefault(number, []).append(component)
if set(groups) != set(purchased) or set(groups) != set(sourced):
raise ValueError('Fitted identities must exactly match dated purchasing and sourcing records')
parts = []
for number, components in groups.items():
observation = sourced[number]
references = [c['name'] for c in components]
if observation['status'] != 'PASS' or min(observation['stock_fields']['canPresaleNumber'], observation['stock_fields']['overseasStockCount']) < len(components)*5:
raise ValueError(f'{number}: public stock is insufficient for five finished boards')
if set(observation['references']) != set(references) or {c['manufacturer_part_number'] for c in components} != {observation['mpn']}:
raise ValueError(f'{number}: fitted references or manufacturer identity changed')
part = dict(purchased[number])
part.update(reference_designators=references, quantity=len(components),
observed_stock=observation['stock_fields']['canPresaleNumber'],
availability_check_date=datetime.fromisoformat(observation['checked_at_utc']).date().isoformat(),
qualification='Engineering prototype. Dated public stock is not reserved. Supplier assembly/process/cost and physical RF, thermal, enclosure, programming and phone validation remain pending.')
parts.append(part)
bom = {'revision':'R9-bare-ESP32-C3-'+json.loads((ROOT/'package.json').read_text())['version'],
'status':f'{sum(p["quantity"] for p in parts)} fitted references / {len(parts)} exact supplier identities; hardware untested; supplier release gates pending',
'circuit_sha256':hashlib.sha256(raw).hexdigest(), 'parts':parts}
(ROOT/'bom.json').write_text(json.dumps(bom,indent=2)+'\n')
fields = ['reference_designators','quantity','value','manufacturer_part_number','package','lcsc','assembly_category','observed_stock','availability_check_date','source_link','qualification']
with (ROOT/'BOM.csv').open('w',newline='') as output:
writer=csv.DictWriter(output,fieldnames=fields,extrasaction='ignore',lineterminator='\n')
writer.writeheader()
for part in parts: writer.writerow({**part,'reference_designators':','.join(part['reference_designators'])})
price=sum(p['quantity']*p['unit_price_first_tier_usd'] for p in parts)
stock_dates=', '.join(sorted({p['availability_check_date'] for p in parts}))
(ROOT/'BOM.md').write_text('# R9 bare ESP32-C3 fitted BOM\n\n'+bom['status']+'\n\nExact manufacturer identities and references are derived from the native circuit. Public stock was checked on '+stock_dates+' and is not reserved. The protected ASR00012 battery is outside the PCBA BOM. Retained2026-10-13 first-tier prices total approximately US$'+f'{price:.4f}'+' per board, excluding assembly, attrition, shipping, battery, mechanics and required filled/capped-via processing. This is not a supplier quote.\n\n| References | MPN | JLCPCB | Quantity | Observed stock |\n|---|---|---|---:|---:|\n'+''.join(f'| {", ".join(p["reference_designators"])} | {p["manufacturer_part_number"]} | {p["lcsc"]} | {p["quantity"]} | {p["observed_stock"]} |\n' for p in parts))
print(bom['status'])
if __name__=='__main__': write_bom()