import sys,re,json,csv,shutil,hashlib,zipfile
from pathlib import Path
ROOT=Path(__file__).resolve().parent;BASE=ROOT.parent;H=BASE/'engineering_handoff_local'
sys.path.insert(0,str(BASE/'parity_metadata_closure_A22'))
from prepare_candidates import forms,run,sha
from align_and_verify import footprint_rows,netlist
URL='https://assets.nexperia.com/documents/data-sheet/BSS138BK.pdf'
SPECS=[('drill',BASE/'parity_metadata_closure_A22/drill','drill_motor_evt_a2_metadata','drill_motor_evt_a3_logic_fet','Q1','R5'),('vacuum',BASE/'fan_gate_clamp_A28/vacuum_gate_candidate','vacuum_control_evt_a4_gate_clamp','vacuum_control_evt_a5_logic_fet','Q2','R7')]
checks={}
for prod,SRC,OLD,NEW,q,r in SPECS:
 DST=ROOT/prod;DST.mkdir(exist_ok=True)
 for p in SRC.iterdir():
  if p.name.endswith('.pretty'):shutil.copytree(p,DST/p.name,dirs_exist_ok=True)
  elif p.name in ['Project.kicad_sym','sym-lib-table','fp-lib-table']:shutil.copyfile(p,DST/p.name)
 shutil.copyfile(SRC/(OLD+'.kicad_pro'),DST/(NEW+'.kicad_pro'))
 for ext,tag in [('kicad_sch','symbol'),('kicad_pcb','footprint')]:
  s=(SRC/(OLD+'.'+ext)).read_text()
  if ext=='kicad_sch':s=s.replace('(project "'+OLD+'"','(project "'+NEW+'"')
  seen=set()
  for st,en,b in reversed(list(forms(s,tag))):
   if '(property "Reference" "'+q+'"' in b:
    assert '"2N7002"' in b;b=b.replace('"2N7002"','"BSS138BK"');seen.add(q)
   elif '(property "Reference" "'+r+'"' in b:
    assert '"100R"' in b;b=b.replace('"100R"','"680R"');seen.add(r)
   s=s[:st]+b+s[en:]
  assert seen=={q,r};(DST/(NEW+'.'+ext)).write_text(s)
 pcb=DST/(NEW+'.kicad_pcb');sch=DST/(NEW+'.kicad_sch')
 for args,name in [(['sch','export','netlist','--format','kicadxml','--output',str(DST/'netlist.xml'),str(sch)],'netlist.log'),(['sch','erc','--format','json','--output',str(DST/'erc.json'),str(sch)],'erc.log'),(['pcb','drc','--format','json','--schematic-parity','--output',str(DST/'drc.json'),str(pcb)],'drc.log')]:assert run(args,DST/name)==0
 d=json.loads((DST/'drc.json').read_text());e=json.loads((DST/'erc.json').read_text())
 assert not d['violations'] and not d['unconnected_items'] and all(not x['violations'] for x in e['sheets'])
 assert len(d['schematic_parity'])==4 and all(x['type']=='extra_footprint' for x in d['schematic_parity'])
 _,a=netlist(SRC/'netlist.xml');_,b=netlist(DST/'netlist.xml');assert a==b
 old={ref:blk for _,_,blk,ref,_ in footprint_rows((SRC/(OLD+'.kicad_pcb')).read_text())};new={ref:blk for _,_,blk,ref,_ in footprint_rows(pcb.read_text())}
 assert set(old)==set(new) and {ref for ref in old if old[ref]!=new[ref]}=={q,r}
 for ref in [q,r]:
  assert new[ref].replace('"BSS138BK"','"2N7002"').replace('"680R"','"100R"')==old[ref]
 for tag in ['segment','via','zone']:assert [x[2] for x in forms(pcb.read_text(),tag)]==[x[2] for x in forms((SRC/(OLD+'.kicad_pcb')).read_text(),tag)]
 for folder in ['gerbers','drill']:(DST/folder).mkdir(exist_ok=True)
 for args,name in [(['pcb','export','gerbers','--layers','F.Cu,B.Cu,F.Paste,B.Paste,F.Silkscreen,B.Silkscreen,F.Mask,B.Mask,Edge.Cuts','--output',str(DST/'gerbers')+'/',str(pcb)],'gerber-export.log'),(['pcb','export','drill','--format','excellon','--excellon-separate-th','--output',str(DST/'drill')+'/',str(pcb)],'drill-export.log'),(['pcb','export','pos','--format','csv','--units','mm','--side','front','--smd-only','--exclude-dnp','--output',str(DST/'centroid-front.csv'),str(pcb)],'centroid-export.log')]:assert run(args,DST/name)==0
 def layers(folder):return {p.name.split('-',1)[1].rsplit('.',1)[0]:p for p in folder.iterdir() if p.suffix in {'.gbr','.gtl','.gbl','.gts','.gbs','.gtp','.gbp','.gto','.gbo','.gm1'}}
 def geom(p):return '\n'.join(x for x in p.read_text().splitlines() if not x.startswith(('%TF.CreationDate,','%TF.ProjectId,','%TO.N,','G04 Created by KiCad')))
 baseline=ROOT/('comparison_baseline_'+prod);baseline.mkdir(exist_ok=True)
 assert run(['pcb','export','gerbers','--layers','F.Cu,B.Cu,F.Paste,B.Paste,F.Silkscreen,B.Silkscreen,F.Mask,B.Mask,Edge.Cuts','--output',str(baseline)+'/',str(SRC/(OLD+'.kicad_pcb'))],ROOT/(prod+'-baseline-export.log'))==0
 ga=layers(baseline);gb=layers(DST/'gerbers');assert len(gb)==9 and set(ga)==set(gb);assert all(geom(ga[k])==geom(gb[k]) for k in ga)
 for kind in ['PTH','NPTH']:
  a=next((SRC/'drill').glob('*-'+kind+'.drl'));b=next((DST/'drill').glob('*-'+kind+'.drl'));assert [x for x in a.read_text().splitlines() if x.startswith('X')]==[x for x in b.read_text().splitlines() if x.startswith('X')]
 with (DST/'centroid-front.csv').open() as f:pos=list(csv.DictReader(f))
 with (SRC/'centroid-front.csv').open() as f:opos=list(csv.DictReader(f))
 assert len(pos)==len(opos)
 for a,b in zip(pos,opos):
  for k in ['Ref','PosX','PosY','Rot','Side','Package']:assert a[k]==b[k]
 padnets={re.match(r'\(pad "([^\"]+)"',p)[1]:re.search(r'\(net "([^\"]+)"',p)[1] for _,_,p in forms(new[q],'pad')}
 checks[prod]={'board_sha256':sha(pcb),'schematic_sha256':sha(sch),'source_board_sha256':sha(SRC/(OLD+'.kicad_pcb')),'candidate_path':str(pcb.relative_to(BASE)),'logic_fet':q,'series_resistor':r,'pad_nets_G1_S2_D3':padnets,'ERC':0,'DRC':0,'unconnected':0,'parity_mounting_holes':4,'all_connected_pin_nets_unchanged':True,'copper_segments_vias_zones_byte_identical':True,'nine_CAM_layer_geometry_unchanged':True,'CAM_comparison':'Fresh source and candidate exported with identical explicit layer arguments; A22 archived source CAM used different board-plot settings, not reused.','drill_coordinates_unchanged':True,'numeric_centroid_coordinates_unchanged':True,'centroid_rows':len(pos),'physical':'NOT_RUN_NO_SAMPLE','status':'FABRICATION_HOLD'}
checks['primary_source']={'url':URL,'revision':'Rev1 4 August2011; Nexperia republished2017','web_pdf_verified_pages':[2,6],'pin1':'G','pin2':'S','pin3':'D','Rds_on_max_ohm_at25C_Vgs2p5V_Id10mA':6.5,'Qg_max_nC_at25C_Vds30V_Id300mA_Vgs4p5V':.7,'Ciss_max_pF_at25C_Vds10V_Vgs0':56,'direct_PDF_download':'HTTP403; no local PDF hash or redistributed PDF','packaging_suffix':'Not frozen; request manufacturer-confirmed tape/reel ordering code'}
checks['qualification_envelope']={'peak_gate_current_A_arithmetic_at3p6V_R680_1pct':3.6/(680*.99),'low_gate_voltage_min_required_V':2.5,'signal_sink_current_max_A':.01,'max_static_sink_voltage_V_arithmetic_at25C':.01*6.5,'PWM_probe_frequency_Hz':25000,'period_us':40,'not_proven':['Actual MCU VOH/rail/tolerance/temperature at both stages','Actual fan pull-up current and voltage; internal pull-up/rise time and cable capacitance unknown','Hot/cold transistor and reset/off/run waveforms; 25C data does not prove all temperatures','Switching time on actual light signal loads; Qg test conditions differ','PWM polarity,0/100 percent and minimum accepted pulse with actual fan','DRV8316 AVDD/DRVOFF thresholds and reset timing measured on board','SOT23 land/assembly DFM and full system validation']}
(ROOT/'logic_fet_checks.json').write_text(json.dumps(checks,indent=2)+'\n')
def read(p):
 with p.open(encoding='utf-8-sig',newline='') as f:return list(csv.DictReader(f))
def write(p,rows):
 with p.open('w',encoding='utf-8-sig',newline='') as f:w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
rows=read(BASE/'fan_gate_clamp_A28/three_board_candidate_bom_A28.csv')
for prod,SRC,OLD,NEW,q,rref in SPECS:
 for r in rows:
  if r['Product']!=prod:continue
  r['Board']=prod+' logic FET A29 review candidate'
  if r['Ref'] not in [q,rref]:continue
  isfet=r['Ref']==q;r.update({'DesignValue':'BSS138BK' if isfet else '680R','CandidateManufacturer':'Nexperia' if isfet else 'YAGEO','CandidateMPN':'BSS138BK' if isfet else 'RC0603FR-07680RL','SelectionStatus':'CONDITIONAL_LOGIC_FET_APPLICATION_A29' if isfet else 'CONDITIONAL_GPIO_SERIES_RESISTOR_A29','PrimarySourceURL':URL if isfet else 'https://yageogroup.com/download/specsheet/RC0603FR-07680RL','PrimarySourceSHA256':'' if isfet else sha(BASE/'fan_gate_clamp_A28/sources/RC0603FR-07680RL.pdf'),'ChecksPerformed':'Manufacturer G1/S2/D3,2.5V Rds maximum and low Qg maximum reviewed; exact net/pad match and fresh CAD native checks. Ordering suffix not frozen.' if isfet else '680Ohm,1%,0603,0.1W70C official spec; assumed3.6V peak gate-current arithmetic only.','RemainingQualification':'; '.join(checks['qualification_envelope']['not_proven']),'QuoteStatus':'NO_FACTORY_OR_1K_PRICE_VERIFIED_A29','UnitPriceUSD1000':''})
assert len(rows)==111 and sum(bool(r['CandidateMPN']) for r in rows)==90
write(ROOT/'three_board_candidate_bom_A29.csv',rows)
for prod in ['drill','vacuum','lamp']:write(ROOT/(prod+'_candidate_bom_A29.csv'),[r for r in rows if r['Product']==prod])
oldrfq=read(BASE/'fan_gate_clamp_A28/three_board_1000_each_RFQ_A28.csv');rfq=[]
for mpn in sorted({r['CandidateMPN'] for r in rows if r['CandidateMPN']}):
 selected=[r for r in rows if r['CandidateMPN']==mpn];r={k:'' for k in oldrfq[0]};r.update({'ManufacturerCandidate':selected[0]['CandidateManufacturer'],'MPN':mpn,'Status':'CONDITIONAL_CANDIDATE_NOT_AVL_OR_ORDER'})
 for prod,col in [('drill','DrillQtyFor1000'),('vacuum','VacuumQtyFor1000'),('lamp','LampQtyFor1000')]:r[col]=str(sum(int(x['QtyPerBoard'])*1000 for x in selected if x['Product']==prod))
 r['TotalRFQQty']=str(sum(int(r[k]) for k in ['DrillQtyFor1000','VacuumQtyFor1000','LampQtyFor1000']));rfq.append(r)
assert sum(int(x['TotalRFQQty']) for x in rfq)==90000;write(ROOT/'three_board_1000_each_RFQ_A29.csv',rfq)
report=H/'A29-logic-FET-review.md';report.write_text('''# A29 电磨关断逻辑与吸尘器 PWM 信号开关修订

状态：FABRICATION_HOLD。没有下单或物理验证，不放行通电打样。

电磨 Q1 的漏极连接 DRV_OFF、源极接地，栅极通过R5接运行请求；R15从DRV_AVDD上拉DRV_OFF。吸尘器Q2漏极连接J2的FAN_PWM，源极接地，栅极通过R7接MCU PWM。两处均为信号下拉开关，不承载电机或风机功率电流。

撤销泛称2N7002，采用Nexperia BSS138BK条件候选，并将R5/R7从100Ω调整为680Ω1%。原厂资料第2页明确G1/S2/D3；第6页给出25°C、Vgs2.5V、Id10mA下Rds最大6.5Ω，以及指定测试条件下Qg最大0.7nC。此处避免仅根据阈值电压判断导通，也避免用大栅极电荷功率MOS管替代PWM信号开关。未确认采购包装后缀；候选系列名不能作为已冻结订购代码。

按假定GPIO最高3.6V、680Ω1%最小673.2Ω，初始充放电峰值算术上限5.35mA；实际MCU输出能力、Voh、轨容差、温度与栅极漏电仍需逐项核实。条件使用要求实际最低栅压≥2.5V、信号下拉电流≤10mA；在25°C静态条件下算术低电平≤65mV。该值不能推广到实际风机、所有温度或动态波形。

吸尘器固件候选PWM为25kHz、周期40µs，当前目标镜像仍硬锁风机电源和非零占空比。Q2反相下拉使风机信号占空比与MCU占空比互补；实际风机的内置上拉、PWM电压/电流、0/100%行为、最低脉宽、转速曲线尚未获得。不能因为器件Qg小或静态电平计算通过就解除固件锁，也不能把这个四线风机接口假设当作与Amazon原产品等效的事实。

本版两块板原理图和PCB同时更新。新鲜ERC0、物理DRC0、未连接0，仅各保留四个安装孔一致性提示。所有连接引脚网表不变，既有走线/过孔/区域逐字节相同；除Q1/R5及Q2/R7的Value属性外封装块不变。以相同显式参数重新导出源板与候选板的九层Gerber，比较几何完全相同；A22历史Gerber使用板内plot参数并带钻孔标记，不可直接与本轮导出混比，本包未复用该历史文件。钻孔和数值贴装坐标相同，贴装Value已更新，须使用新版BOM/贴装文件。

三款条件候选覆盖90/111：电磨32/39、吸尘器36/44、灯22/28。没有新增器件，新增一种两款共用候选系列；各千台询价数量为90,000个已标识位号。所有价格仍空白，不能据此宣称整机成本降低或量产达标。

原厂依据：https://assets.nexperia.com/documents/data-sheet/BSS138BK.pdf 。Web已读取原厂PDF；本地直接下载返回HTTP403，因此源PDF哈希为空，ZIP不含原厂PDF。680Ω依据沿用A28的YAGEO官方规格和哈希。完整检查、假设与未证事项见logic_fet_checks.json。

下一步是MCU与驱动IC的电平保证条件、接口稳压/拉升、全板应用审核，以及实物波形/热/EMC/DFM和三款原产品同台对比。没有这些证据，功能效果一致和落地量产目标仍未完成。
''',encoding='utf-8')
files=sorted([report]+[p for p in ROOT.rglob('*') if p.is_file() and 'sources' not in p.parts and '__pycache__' not in p.parts and not any(x.startswith('comparison_baseline_') for x in p.parts)]);manifest=''.join(sha(p)+'  A29/'+p.relative_to(BASE).as_posix()+'\n' for p in files);zp=H/'Drill-vacuum-logic-FET-CAD-and-BOM-A29.zip'
with zipfile.ZipFile(zp,'w',zipfile.ZIP_DEFLATED) as z:
 for p in files:z.write(p,'A29/'+p.relative_to(BASE).as_posix())
 z.writestr('A29/SHA256SUMS.txt',manifest)
with zipfile.ZipFile(zp) as z:
 assert z.testzip() is None
 for ln in z.read('A29/SHA256SUMS.txt').decode().splitlines():
  dig,n=ln.split('  ',1);assert hashlib.sha256(z.read(n)).hexdigest()==dig
(H/'A29-SHA256SUMS.txt').write_text(manifest);p=H/'LOCAL-PACKAGES.json';idx=json.loads(p.read_text());entry={'archive':zp.name,'sha256':sha(zp),'bytes':zp.stat().st_size,'zip_entries':len(files)+1,'manifest_entries':len(files),'zip_crc':'PASS','manifest_readback':'PASS','mpn_coverage':'90/111 conditional candidates; BSS138BK ordering suffix not frozen','native_checks':'Both: ERC0;DRC0;unconnected0;4mountingholes','physical_validation':'NOT_RUN_NO_SAMPLE','manufacturing_status':'FABRICATION_HOLD'}
idx['packages']=[x for x in idx['packages'] if x.get('archive')!=zp.name]+[entry];idx.update({'status':'A29_LOGIC_FET_CONDITIONAL_CANDIDATE_90_OF_111_FABRICATION_HOLD','latest_report':report.name,'latest_machine_report':'../logic_fet_A29/logic_fet_checks.json','latest_manifest':'A29-SHA256SUMS.txt'})
for prod,key in [('drill','drill_motor'),('vacuum','vacuum_control')]:idx['latest_candidate_sources'][key]='../'+checks[prod]['candidate_path']
idx['latest_bom_candidates']={k:'../logic_fet_A29/'+k+'_candidate_bom_A29.csv' for k in ['drill','vacuum','lamp']};idx['logic_fet_gate_A29']={'status':'CONDITIONAL_REPLACEMENT_IN_CAD_NATIVE_CHECKED_NOT_PHYSICAL_QUALIFICATION','powered_test_release':False,'source_pdf_local_hash':'unavailable HTTP403','open_conditions':checks['qualification_envelope']['not_proven']};p.write_text(json.dumps(idx,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'package':entry,'RFQ_groups':len(rfq),'boards':checks},indent=2))
