import sys,json,csv,re,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 run,sha,forms
from align_and_verify import footprint_rows,netlist
DST=ROOT/'vacuum_gate_candidate';SRC=BASE/'esd_topology_A27/vacuum_esd_candidate';NEW='vacuum_control_evt_a4_gate_clamp';OLD='vacuum_control_evt_a3_esd_railfree';pcb=DST/(NEW+'.kicad_pcb')
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'])
for n in ['gerbers','drill']:(DST/n).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
old={r:b for _,_,b,r,_ in footprint_rows((SRC/(OLD+'.kicad_pcb')).read_text())};new={r:b for _,_,b,r,_ in footprint_rows(pcb.read_text())}
assert set(new)-set(old)=={'R17'} and {r for r in old if old[r]!=new[r]}=={'Q4','R14'}
for r in old:assert re.search(r'\(at [^)]*\)',old[r])[0]==re.search(r'\(at [^)]*\)',new[r])[0]
padnets={}
for r in ['Q4','R17','D1','R16']:
 padnets[r]={re.match(r'\(pad "([^\"]+)"',p)[1]:re.search(r'\(net "([^\"]+)"',p)[1] for _,_,p in forms(new[r],'pad')}
assert padnets['Q4']['3']==padnets['R17']['2']=='/FAN_GATE_SINK';assert padnets['R17']['1']==padnets['D1']['2']==padnets['R16']['1']=='/FAN_FET_GATE'
_,pins=netlist(DST/'netlist.xml');assert {k for k,v in pins.items() if v=='/FAN_GATE_SINK'}=={('Q4','3'),('R17','2')}
def read(p):
 with p.open(encoding='utf-8-sig',newline='') as f:return list(csv.DictReader(f))
def write(p,rs):
 with p.open('w',encoding='utf-8-sig',newline='') as f:w=csv.DictWriter(f,fieldnames=list(rs[0]));w.writeheader();w.writerows(rs)
oldpos={r['Ref']:r for r in read(SRC/'centroid-front.csv')};newpos={r['Ref']:r for r in read(DST/'centroid-front.csv')}
assert len(newpos)==41 and set(newpos)-set(oldpos)=={'R17'}
for ref in oldpos:
 for key in ['PosX','PosY','Rot','Side']:assert newpos[ref][key]==oldpos[ref][key]
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')))
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'}}
a=layers(SRC/'gerbers');b=layers(DST/'gerbers');assert set(a)==set(b) and len(b)==9
changed=[k for k in a if geom(a[k])!=geom(b[k])]
assert set(changed)=={'F_Cu','F_Mask','F_Paste','F_Silkscreen'},changed
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')]
urls={n:'https://www.aosmd.com/res/data_sheets/AO3400A.pdf' if n=='AO3400A' else 'https://yageogroup.com/download/specsheet/'+n for n in ['AO3400A','RC0603FR-07680RL','RC0603FR-073K3L']}
c={'source_board_sha256':sha(SRC/(OLD+'.kicad_pcb')),'candidate_board_sha256':sha(pcb),'candidate_schematic_sha256':sha(DST/(NEW+'.kicad_sch')),'native_erc':0,'native_physical_drc':0,'native_unconnected':0,'remaining_parity':'4 PCB-only mounting holes','pad_nets':padnets,'new_sink_net_has_only_Q4_D3_and_R17_P2':True,'other_pin_nets_unchanged':True,'other_footprint_blocks_unchanged':True,'all_prior_component_positions_unchanged':True,'new_component':'R17 3.3k 1% 0603','R14':'680R 1%','Q4':'AO3400A conditional candidate','math_assumptions':{'bus_V_max_design_envelope_not_measured':17,'R17_min_ohm_25C':3300*.99,'D1_short_DC_current_A_bound_at17V':17/(3300*.99),'D1_short_DC_R17_W_at17V':17**2/(3300*.99),'R17_rating_W_at70C':.1,'GPIO_V_max_assumed':3.6,'R14_min_ohm_25C':680*.99,'GPIO_peak_A_bound':3.6/(680*.99),'not_included':'Temperature coefficient, rail/output tolerance validation, transient/pulse energy, PCB derating; no actual timing or waveform evidence'},'cam':{'gerber_layers':9,'changed_layers':changed,'centroid_rows':41,'added_centroid':newpos['R17'],'all_prior_numeric_centroid_coordinates_unchanged':True,'drill_coordinates_unchanged':True},'sources':{n:{'url':u,'sha256':sha(ROOT/'sources'/(n+'.pdf'))} for n,u in urls.items()},'physical_validation':'NOT_RUN_NO_SAMPLE','manufacturing_status':'FABRICATION_HOLD','powered_test_release':False}
(ROOT/'gate_clamp_checks.json').write_text(json.dumps(c,indent=2)+'\n')
rows=read(BASE/'esd_topology_A27/three_board_candidate_bom_A27.csv')
remain='Whole-board review and physical GPIO/Vgs/PMOS gate waveforms, cold/hot start, stall/transient/thermal tests; 17V is calculation envelope, not measured or approved input; not AVL or manufacturing release.'
for r in rows:
 if r['Product']!='vacuum':continue
 r['Board']='Vacuum A4 gate clamp A28 candidate'
 if r['Ref'] in ['Q4','R14']:
  mpn='AO3400A' if r['Ref']=='Q4' else 'RC0603FR-07680RL'
  r.update({'DesignValue':'AO3400A' if r['Ref']=='Q4' else '680R','CandidateManufacturer':'Alpha & Omega Semiconductor' if r['Ref']=='Q4' else 'YAGEO','CandidateMPN':mpn,'SelectionStatus':'CONDITIONAL_GATE_APPLICATION_CANDIDATE_A28','PrimarySourceURL':urls[mpn],'PrimarySourceSHA256':c['sources'][mpn]['sha256'],'ChecksPerformed':'AO3400A G/S/D top view checked against existing G1/S2/D3 footprint; max Rds specified at Vgs2.5V,25C. R17 limits drain branch. No physical qualification.' if r['Ref']=='Q4' else 'Manufacturer exact 680 Ohm,1%,0603,0.1W at70C specification; GPIO peak current arithmetic under explicit voltage assumption.','RemainingQualification':remain,'UnitPriceUSD1000':'','QuoteStatus':'NO_FACTORY_OR_1K_PRICE_VERIFIED_A28'})
r=next(r for r in rows if r['Product']=='vacuum' and r['Ref']=='R14').copy();r.update({'Ref':'R17','DesignValue':'3.3k','CandidateMPN':'RC0603FR-073K3L','PrimarySourceURL':urls['RC0603FR-073K3L'],'PrimarySourceSHA256':c['sources']['RC0603FR-073K3L']['sha256'],'ChecksPerformed':'Added series clamp-current limiter. Exact 3.3k,1%,0603,0.1W at70C spec. Worst DC short arithmetic recorded; temperature/actual bus/pulse conditions open.'});rows.append(r)
assert len(rows)==111 and sum(bool(r['CandidateMPN']) for r in rows)==88
write(ROOT/'three_board_candidate_bom_A28.csv',rows)
for p in ['drill','vacuum','lamp']:write(ROOT/(p+'_candidate_bom_A28.csv'),[r for r in rows if r['Product']==p])
rfq0=read(BASE/'esd_topology_A27/three_board_1000_each_RFQ_A27.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 rfq0[0]};r.update({'ManufacturerCandidate':selected[0]['CandidateManufacturer'],'MPN':mpn,'Status':'CONDITIONAL_CANDIDATE_NOT_AVL_OR_ORDER'})
 for p,col in [('drill','DrillQtyFor1000'),('vacuum','VacuumQtyFor1000'),('lamp','LampQtyFor1000')]:r[col]=str(sum(int(x['QtyPerBoard'])*1000 for x in selected if x['Product']==p))
 r['TotalRFQQty']=str(sum(int(r[k]) for k in ['DrillQtyFor1000','VacuumQtyFor1000','LampQtyFor1000']));rfq.append(r)
assert sum(int(r['TotalRFQQty']) for r in rfq)==88000;write(ROOT/'three_board_1000_each_RFQ_A28.csv',rfq)
report=H/'A28-gate-clamp-review.md'
report.write_text('''# A28 吸尘器风机电源栅极限流修订

状态：FABRICATION_HOLD，尚未放行通电打样。没有样板波形、温升、原产品功能对比，也没有下单。

## 修复原因与实际 CAD 变更

A27 中 Q4 漏极直接下拉 Q3 功率 PMOS 栅极；D1 的阴极在 VBUS_SW、阳极在 FAN_FET_GATE。R16 是源栅并联上拉，不能限制 VBUS→D1→Q4→GND 支路电流。直接替换低导通电阻 Q4 会加重该问题。

本版增加 R17=3.3kΩ、1%、0603 串联在 FAN_FET_GATE 与新 FAN_GATE_SINK 之间，Q4 漏极仅接 FAN_GATE_SINK。R17 在 PCB (56,53.5)mm，修改两处局部前铜线路。Q4 换为 AO3400A 条件候选，G1/S2/D3 与现有符号和封装方向核对；R14 从100Ω调整为680Ω以限制 MCU 栅极充放电峰值。电磨 Q1、吸尘器 PWM Q2 保持待审核，不能把这个慢速电源开关结论用于 PWM。

## 计算边界

R17 最小值按25°C、1%容差为3267Ω；即使 D1 短路，在假定17V直流包络下支路≤5.204mA、电阻功耗≤88.46mW。该料原厂标称0.1W是在70°C，温度系数和更高温降额、输入实际范围及脉冲能量仍需审核。17V仅为计算包络，不代表整板允许17V输入。12V下相同故障功耗约44.08mW。

R14最小673.2Ω，在假定GPIO最高3.6V下充放电初始峰值≤5.35mA。实际 MCU 输出规格、3.3V轨容差、栅极低温/高温电压和动态波形未完成验证，不能据此宣称驱动已闭环。AO3400A 的2.5V导通电阻指标用于条件选型，其30V漏源、±12V栅源绝对最大值不等于安全工作裕量；需要实际插拔/停机尖峰测试。

D1仍没有冻结具体厂商/料号，10V标称不证明在本电流下的实际稳压值。Q3栅极绝对最大裕量、开启/关闭速度、软启动和风机电流必须台架核实。R17控制正常稳压电流和短路支路，不构成全板故障防护结论。

## 已完成的本地检查

新鲜 KiCad ERC 0、物理DRC 0、未连接0，原理图/PCB一致性只保留四个PCB安装孔提示。网表证明新 FAN_GATE_SINK 仅包含Q4漏极与R17的2脚，其他既有引脚网络不变。其他封装块逐字节不变，既有器件位置不变。

重新导出9层Gerber、钻孔和41个贴装坐标。前铜、前阻焊、前钢网、前丝印变化；其他层几何及钻孔坐标不变。新增R17贴装点，既有40个贴装点数值坐标不变。不得混用旧前铜/钢网/贴装文件。

## 成本和剩余工作

三款条件候选料号为88/111：电磨31/39、吸尘器35/44、灯22/28。按各1000台重新汇总询价用量88,000个已标识器件位号，新增一只必要限流电阻；全部报价留空，无整机成本达标结论。未定具体料号、负载接口、固件、机械、散热、EMC/DFM与实物等效验证仍需闭环。

原厂资料来源、SHA256和计算假设见 gate_clamp_checks.json。原厂PDF本地保存，不在交付ZIP中分发。本包是评审候选，不能直接作为工厂制造或通电放行文件。
''',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])
manifest=''.join(sha(p)+'  A28/'+p.relative_to(BASE).as_posix()+'\n' for p in files);zp=H/'Vacuum-gate-clamp-CAD-and-BOM-A28.zip'
with zipfile.ZipFile(zp,'w',zipfile.ZIP_DEFLATED) as z:
 for p in files:z.write(p,'A28/'+p.relative_to(BASE).as_posix())
 z.writestr('A28/SHA256SUMS.txt',manifest)
with zipfile.ZipFile(zp) as z:
 assert z.testzip() is None
 for ln in z.read('A28/SHA256SUMS.txt').decode().splitlines():
  h,n=ln.split('  ',1);assert hashlib.sha256(z.read(n)).hexdigest()==h
(H/'A28-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':'88/111 conditional candidates','native_checks':'ERC 0; DRC 0; unconnected 0; parity 4 mounting holes','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':'A28_VACUUM_GATE_CLAMP_LIMITER_CANDIDATE_88_OF_111_FABRICATION_HOLD','latest_report':report.name,'latest_machine_report':'../fan_gate_clamp_A28/gate_clamp_checks.json','latest_manifest':'A28-SHA256SUMS.txt'})
idx['latest_candidate_sources']['vacuum_control']='../fan_gate_clamp_A28/vacuum_gate_candidate/'+NEW+'.kicad_pcb';idx['latest_bom_candidates']={k:'../fan_gate_clamp_A28/'+k+'_candidate_bom_A28.csv' for k in ['drill','vacuum','lamp']}
idx['manufacturing_exclusions']['vacuum_A27']='Gate-clamp current branch lacks series limiting resistor; superseded by A28 review candidate. Neither revision approved for manufacture.'
idx['vacuum_gate_drive_application_gate']={'status':'A28_SERIES_CLAMP_LIMITER_ADDED_PHYSICAL_WAVEFORM_NOT_RUN','powered_test_release':False,'remaining':'D1 exact MPN, MCU output envelope, dynamic/thermal/transient whole-board audit; Q2/drillQ1 low-Vgs qualification open'}
p.write_text(json.dumps(idx,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'package':entry,'RFQ_groups':len(rfq),'checks':c},indent=2))
