from pathlib import Path
import sys,re,json,csv,shutil,zipfile,itertools
R=Path(__file__).resolve().parent;B=R.parent;H=B/'engineering_handoff_local';sys.path.insert(0,str(B/'parity_metadata_closure_A22'))
from prepare_candidates import forms,run,sha
from align_and_verify import footprint_rows,netlist
S=B/'logic_fet_A29/drill';O='drill_motor_evt_a3_logic_fet';N='drill_motor_evt_a4_bus_divider';D=R/'drill';D.mkdir(exist_ok=True)
for p in S.iterdir():
 if p.name.endswith('.pretty'):shutil.copytree(p,D/p.name,dirs_exist_ok=True)
 elif p.name in ['Project.kicad_sym','sym-lib-table','fp-lib-table']:shutil.copyfile(p,D/p.name)
shutil.copyfile(S/(O+'.kicad_pro'),D/(N+'.kicad_pro'))
for ext,tag in [('kicad_sch','symbol'),('kicad_pcb','footprint')]:
 s=(S/(O+'.'+ext)).read_text()
 if ext=='kicad_sch':s=s.replace('(project "'+O+'"','(project "'+N+'"')
 changed=0
 for st,en,x in reversed(list(forms(s,tag))):
  if '(property "Reference" "R13"' not in x:continue
  assert '"100k 1%"' in x;x=x.replace('"100k 1%"','"240k 1%"');s=s[:st]+x+s[en:];changed+=1
 assert changed==1;(D/(N+'.'+ext)).write_text(s)
p=D/(N+'.kicad_pcb');sch=D/(N+'.kicad_sch')
for args,name in [(['sch','export','netlist','--format','kicadxml','--output',str(D/'netlist.xml'),str(sch)],'netlist.log'),(['sch','erc','--format','json','--output',str(D/'erc.json'),str(sch)],'erc.log'),(['pcb','drc','--format','json','--schematic-parity','--output',str(D/'drc.json'),str(p)],'drc.log')]:assert run(args,D/name)==0
j=json.loads((D/'drc.json').read_text());e=json.loads((D/'erc.json').read_text());assert not j['violations'] and not j['unconnected_items'];assert len(j['schematic_parity'])==4 and all(v['type']=='extra_footprint' for v in j['schematic_parity']);assert not [v for x in e['sheets'] for v in x['violations']]
_,a=netlist(S/'netlist.xml');_,b=netlist(D/'netlist.xml');assert a==b
s0=(S/(O+'.kicad_pcb')).read_text();s1=p.read_text()
for tag in ['segment','via','zone']:assert [x[2] for x in forms(s0,tag)]==[x[2] for x in forms(s1,tag)]
a={r:f for _,_,f,r,_ in footprint_rows(s0)};b={r:f for _,_,f,r,_ in footprint_rows(s1)};assert set(a)==set(b);assert {r for r in a if a[r]!=b[r]}=={'R13'};assert b['R13'].replace('"240k 1%"','"100k 1%"')==a['R13']
for folder in ['gerbers','drill']:(D/folder).mkdir(exist_ok=True)
lay='F.Cu,B.Cu,F.Paste,B.Paste,F.Silkscreen,B.Silkscreen,F.Mask,B.Mask,Edge.Cuts'
for args,name in [(['pcb','export','gerbers','--layers',lay,'--output',str(D/'gerbers')+'/',str(p)],'gerber-export.log'),(['pcb','export','drill','--format','excellon','--excellon-separate-th','--output',str(D/'drill')+'/',str(p)],'drill-export.log'),(['pcb','export','pos','--format','csv','--units','mm','--side','front','--smd-only','--exclude-dnp','--output',str(D/'centroid-front.csv'),str(p)],'centroid-export.log')]:assert run(args,D/name)==0
base=R/'baseline_cam';base.mkdir(exist_ok=True);assert run(['pcb','export','gerbers','--layers',lay,'--output',str(base)+'/',str(S/(O+'.kicad_pcb'))],R/'baseline-export.log')==0
suffs={'.gbr','.gtl','.gbl','.gts','.gbs','.gtp','.gbp','.gto','.gbo','.gm1'}
def layers(d):return {p.name.split('-',1)[1].rsplit('.',1)[0]:p for p in d.iterdir() if p.suffix in suffs}
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')))
a,b=layers(base),layers(D/'gerbers');assert len(b)==9 and a.keys()==b.keys();assert all(geom(a[k])==geom(b[k]) for k in a)
for k in ['PTH','NPTH']:
 a=next((S/'drill').glob('*-'+k+'.drl'));b=next((D/'drill').glob('*-'+k+'.drl'));assert [s for s in a.read_text().splitlines() if s.startswith('X')]==[s for s in b.read_text().splitlines() if s.startswith('X')]
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)
a=read(S/'centroid-front.csv');b=read(D/'centroid-front.csv');assert len(a)==len(b)
for x,y in zip(a,b):
 for k in ['Ref','PosX','PosY','Rot','Side','Package']:assert x[k]==y[k]
rows=read(B/'logic_fet_A29/three_board_candidate_bom_A29.csv')
for r in rows:
 if r['Product']=='drill':r['Board']='drill bus divider A34 review candidate'
 if r['Product']!='drill' or r['Ref']!='R13':continue
 r.update({'DesignValue':'240k 1%','CandidateMPN':'RC0603FR-07240KL','SelectionStatus':'CONDITIONAL_BUS_DIVIDER_A34','PrimarySourceURL':'https://yageogroup.com/download/specsheet/RC0603FR-07240KL','PrimarySourceSHA256':sha(R/'sources/RC0603FR-07240KL.pdf'),'ChecksPerformed':'Manufacturer240k1%0603,0.1W70C,75V,100ppm/C; CAD nets unchanged; static divider corner calculation only','RemainingQualification':'Actual VDDA and unpowered sequencing; bus rating/transient/clamp; ADC sample timing and calibration; temperature/DFM; supplier price','UnitPriceUSD1000':'','QuoteStatus':'NO_FACTORY_OR_1K_PRICE_VERIFIED_A34'})
assert len(rows)==111 and sum(bool(r['CandidateMPN']) for r in rows)==90
write(R/'three_board_candidate_bom_A34.csv',rows)
for prod in ['drill','vacuum','lamp']:write(R/(prod+'_candidate_bom_A34.csv'),[r for r in rows if r['Product']==prod])
rfq0=read(B/'logic_fet_A29/three_board_1000_each_RFQ_A29.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 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(r['TotalRFQQty']) for r in rfq)==90000;write(R/'three_board_1000_each_RFQ_A34.csv',rfq)
rt,rb=240000,22000;lo=rb*.99/(rt*1.01+rb*.99);hi=rb*1.01/(rt*.99+rb*1.01);ratio=rb/(rt+rb);rth=rt*rb/(rt+rb)
checks={'status':'A34_CONDITIONAL_DIVIDER_CAD_NATIVE_PASS_NOT_BOARD_VOLTAGE_RELEASE','board_sha256':sha(p),'schematic_sha256':sha(sch),'ERC':0,'DRC':0,'unconnected':0,'parity_mounting_holes':4,'changed_ref':'R13','old_new_ohm':[100000,240000],'MPN':'RC0603FR-07240KL','source_pdf_sha256':sha(R/'sources/RC0603FR-07240KL.pdf'),'all_connected_pin_nets':'UNCHANGED','Cu_segments_vias_zones':'BYTE_IDENTICAL','nine_Gerber_geometry':'IDENTICAL','drill_coordinates':'IDENTICAL','centroid_numeric_geometry':'IDENTICAL','BOM_refs':39,'whole_product_cost':'NOT_ESTIMABLE; selected board BOM identities only; all factory prices blank','divider':{'nominal':ratio,'ratio_min_1pct':lo,'ratio_max_1pct':hi,'ADC_voltage_35V_max_1pct':35*hi,'ADC_margin_to_assumed3p135V':3.135-35*hi,'nominal_resolution_mV_per12bitLSB_at3p3V':3300/4095/ratio,'old_resolution_mV_perLSB':3300/4095/(22000/122000),'thevenin_ohm':rth,'filter_tau_ms_nominal_C18_10n':rth*1e-5,'5tau_settle_ms_nominal':5*rth*1e-5,'upper_resistor_35V_power_mW_max_1pct':max(35**2*t/(t+l)**2*1000 for t,l in itertools.product([rt*.99,rt*1.01],[rb*.99,rb*1.01])),'qualification':'Only1pct room-temperature tolerance included; TCR,aging,VDDA transient and unpowered injection not included.3.135V is an assumed calculation bound,not actual rail guarantee.'},'ADC_implementation':'ABSENT; targetmain hard-off. No old bus-conversion scale exists in active firmware. Future conversion must use262/22 and calibratedVDDA; no firmware or guard changed this turn.','ADC_sampling_candidate':'ST DS12589 Table61: at60MHz12bit640.5cycle, slow-channel RAINmax33k; divider~20.15k nominal. Candidate only; actual channel,timing,filter and switched sampling not validated.','not_proven':['Reference motor and battery supply','Board input voltage range and transient/clamp','Unpowered MCU injection and power sequencing','ADC calibration,errorbudget,sampletime and bus shutdown implementation','Thermal/EMC/DFM and full product fit','50kRPM loaded performance and10h runtime','Whole-unit low cost at1000each'],'physical_validation':'NOT_RUN_NO_SAMPLE','manufacturing_status':'FABRICATION_HOLD'}
(R/'checks.json').write_text(json.dumps(checks,ensure_ascii=False,indent=2)+'\n')
report=H/'A34-drill-bus-divider-review.md';report.write_text('''# A34 电磨母线采样分压条件候选

状态：CAD原生检查通过；FABRICATION_HOLD。仅移除一个已发现的静态采样范围冲突，没有整机电压或功能放行。

电机板R13从100k1%改为240k1%，R14仍22k1%。YAGEO候选RC0603FR-07240KL原厂单页确认240k、1%、0603、0.1W@70℃、75V及±100ppm/℃。不增加器件数量、不改封装与布线；工厂价格未取得，不能宣称价格相同或整机成本已达标。

原生ERC0、DRC0、未连接0；4项原理图一致性提示仍仅为PCB独有安装孔。全部引脚网络相同，铜线/过孔/地平面字节一致。9层Gerber几何、PTH/NPTH孔坐标和数值贴装位置逐项比较未变。贴装料值与BOM已更新，旧100k料单不可用来装A34。

按电阻各1%在室温静态计算，35V算例的ADC端最大约2.99323V，低于假设3.135V参考下限。此处35V来自驱动芯片审核范围，只用于证明分压修正效果；不代表35V整板额定值。温漂、老化、真实VDDA、瞬态和MCU未上电注入均不在此静态结论内。

分压更高降低ADC分辨率：3.3V/12bit算例约9.6mV母线/LSB，旧版约4.47mV/LSB。名义等效源阻抗约20.15k、10nF滤波时间常数约0.202ms；测量误差、采样保持充电与保护响应必须验证。ST DS12589 Table61在60MHz/12bit/640.5cycle条件给出慢通道最大33k源阻抗，作为后续采样时间候选依据，不代表ADC实现或实测已完成。

当前A33固件仍仅硬关闭启动，没有实际母线ADC采样、换算和过压关断链。不能声称过压保护已经接通。未来软件换算必须使用262/22及校准后的VDDA；硬件分压不是过压保护器，22V驱动OVP也不能替代MCU输入保护。参考电机、电池串数和母线再生能量未确认；决定整机范围时需要同时审核电容额定值、驱动OVP、TVS/吸能和电池路径。

当前配对：A34电机板 + A32 PF1控制板 + A33硬关闭固件；控制板/固件未修改。90/111候选身份覆盖仍只适用于所选电机/吸尘/灯电源板，不含控制板24个位号及其余整机。千台RFQ重新按MPN聚合，价格继续留空。本轮没有下单、联系供应商、解锁或上电。

依据：https://yageogroup.com/download/specsheet/RC0603FR-07240KL ；https://www.st.com/resource/en/datasheet/stm32g431cb.pdf 。本地原厂电阻PDF仅记录SHA256，不在ZIP分发。计算细节、假设及未证事项见checks.json。
''',encoding='utf-8')
files=[report]+[p for p in R.rglob('*') if p.is_file() and 'sources' not in p.parts and 'baseline_cam' not in p.parts and '__pycache__' not in p.parts];manifest=''.join(sha(p)+'  A34/'+p.relative_to(B).as_posix()+'\n' for p in sorted(files));zp=H/'Drill-bus-divider-CAD-and-BOM-A34.zip'
with zipfile.ZipFile(zp,'w',zipfile.ZIP_DEFLATED) as z:
 for p in sorted(files):z.write(p,'A34/'+p.relative_to(B).as_posix())
 z.writestr('A34/SHA256SUMS.txt',manifest)
with zipfile.ZipFile(zp) as z:
 assert z.testzip() is None
 for ln in z.read('A34/SHA256SUMS.txt').decode().splitlines():dig,n=ln.split('  ',1);assert __import__('hashlib').sha256(z.read(n)).hexdigest()==dig
(H/'A34-SHA256SUMS.txt').write_text(manifest);ip=H/'LOCAL-PACKAGES.json';idx=json.loads(ip.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','native_checks':'ERC0 DRC0 unconnected0 parity4 mountingholes','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':'A34_DRILL_CONDITIONAL_ADC_DIVIDER_CAD_FABRICATION_HOLD','latest_report':report.name,'latest_machine_report':'../bus_divider_A34/checks.json','latest_manifest':'A34-SHA256SUMS.txt'});idx['latest_candidate_sources']['drill_motor']='../bus_divider_A34/drill/'+N+'.kicad_pcb';idx['latest_bom_candidates']={k:'../bus_divider_A34/'+k+'_candidate_bom_A34.csv' for k in ['drill','vacuum','lamp']};idx['latest_drill_safe_firmware_A33']['board_pair']='A34 motor + A32 PF1 controller';idx['drill_ADC_VM_gate_A34']=checks['divider'];idx['manufacturing_exclusions']['drill_A29_R13_100k']='A34 supersedes static divider candidate; old100k BOM must not populate A34. Neither board has manufacturing release.';ip.write_text(json.dumps(idx,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'package':entry,'divider':checks['divider'],'RFQ_groups':len(rfq)},ensure_ascii=False,indent=2))
