from pathlib import Path
import sys,json,csv,re,xml.etree.ElementTree as E,hashlib,zipfile
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 netlist,footprint_rows
DST=ROOT/'controller_candidate';SRC=BASE/'compact_bench_suite/drill_control';NEW='drill_control_evt_a2_paired';OLD='drill_control_evt_a1_compact_bench';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 not d['schematic_parity'] and all(not x['violations'] for x in e['sheets'])
_,cp=netlist(DST/'netlist.xml');_,mp=netlist(BASE/'logic_fet_A29/drill/netlist.xml')
def pins_for(p,ref):return {k[1]:v for k,v in p.items() if k[0]==ref}
assert pins_for(cp,'J1')==pins_for(mp,'J1');assert len(pins_for(cp,'J1'))==20
for b,p,ref in [(pcb,cp,'J1'),(BASE/'logic_fet_A29/drill/drill_motor_evt_a3_logic_fet.kicad_pcb',mp,'J1')]:
 blk=next(x for _,_,x,r,_ in footprint_rows(b.read_text()) if r==ref)
 padnets={re.match(r'\(pad "([^\"]+)"',x)[1]:re.search(r'\(net "([^\"]+)"',x)[1] for _,_,x in forms(blk,'pad')};assert padnets==pins_for(p,ref)
# Manufacturer LQFP48 mapping, independent of the project's custom symbol.
funcs=['VBAT','PC13','PC14','PC15','PF0','PF1','PG10/NRST','PA0','PA1','PA2','PA3','PA4','PA5','PA6','PA7','PB0','PB1','PB2','VSSA','VREF+','VDDA','PB10','VSS','VDD','PB11','PB12','PB13','PB14','PB15','PA8','PA9','PA10','PA11','PA12','VSS','VDD','PA13','PA14','PA15','PB3','PB4','PB5','PB6','PB7','PB8/BOOT0','PB9','VSS','VDD']
cf=pins_for(cp,'U1');assert len(cf)==48
f=next(x for _,_,x,r,_ in footprint_rows(pcb.read_text()) if r=='U1');pads={re.match(r'\(pad "([^\"]+)"',x)[1]:re.search(r'\(net "([^\"]+)"',x)[1] for _,_,x in forms(f,'pad')};assert {k:v.replace('{slash}','/') for k,v in pads.items()}==cf
mcurows=[{'PackagePin':i,'ManufacturerFunction':funcs[i-1],'SchematicNet':cf[str(i)],'PCBNet':pads[str(i)],'Status':'PIN_NET_REVIEW_NOT_PHYSICAL_TEST'} for i in range(1,49)]
rows=[]
for i in range(1,21):
 n=cp[('J1',str(i))];end=[(k[1],funcs[int(k[1])-1]) for k,v in cp.items() if k[0]=='U1' and v==n]
 rows.append({'ConnectorPin':i,'ControllerNet':n,'MotorNet':mp[('J1',str(i))],'MCUPackagePinAndFunction':';'.join(a+':'+b for a,b in end),'MotorEndpoints':';'.join(k[0]+'.'+k[1] for k,v in mp.items() if v==n and k[0]!='J1'),'ReviewStatus':'LOGICAL_AND_PCB_PAD_PIN_MATCH; CABLE_ORIENTATION_AND_PHYSICAL_LEVELS_NOT_RUN'})
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)
write(ROOT/'motor_controller_J1_20pin_review.csv',rows);write(ROOT/'STM32G431_LQFP48_pin_net_review.csv',mcurows)
# Expose the additional control-board cost scope without inventing quotations.
x=E.parse(DST/'netlist.xml');bom=[]
for comp in x.findall('./components/comp'):
 bom.append({'Product':'drill','Board':'control A31 candidate','Ref':comp.attrib['ref'],'QtyPerBoard':1,'DesignValue':comp.findtext('value'),'Footprint':comp.findtext('footprint'),'CandidateMPN':'STM32G431CBT6' if comp.attrib['ref']=='U1' else '', 'Qualification':'MANUFACTURER_PIN_FUNCTION_REVIEW_ONLY' if comp.attrib['ref']=='U1' else 'MPN_APPLICATION_AND_MECHANICAL_OPEN','QtyFor1000':1000,'UnitPriceUSD1000':'','QuoteStatus':'NO_FACTORY_QUOTE; NOT_INCLUDED_IN_90_OF_111_SUBSET'})
assert len(bom)==24;write(ROOT/'drill_control_24refs_cost_scope_A31.csv',bom)
for n in ['gerbers','drill']:(DST/n).mkdir(exist_ok=True)
GARGS=['pcb','export','gerbers','--layers','F.Cu,B.Cu,F.Paste,B.Paste,F.Silkscreen,B.Silkscreen,F.Mask,B.Mask,Edge.Cuts']
assert run(GARGS+['--output',str(DST/'gerbers')+'/',str(pcb)],DST/'gerber-export.log')==0
BL=ROOT/'comparison_baseline';BL.mkdir(exist_ok=True);assert run(GARGS+['--output',str(BL)+'/',str(SRC/(OLD+'.kicad_pcb'))],ROOT/'baseline-gerber.log')==0
for args,name in [(['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
exts={'.gbr','.gtl','.gbl','.gts','.gbs','.gtp','.gbp','.gto','.gbo','.gm1'}
def layers(p):return {f.name.split('-',1)[1].rsplit('.',1)[0]:f for f in p.iterdir() if f.suffix in exts}
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=layers(BL);b=layers(DST/'gerbers');assert len(a)==len(b)==9 and set(a)==set(b);assert all(geom(a[k])==geom(b[k]) for k in a)
ver=json.loads((ROOT/'firmware/固件/drill_target/build/build_verification.json').read_text())
c=json.loads((ROOT/'controller_pair_checks.json').read_text());c.update({'native_ERC':0,'native_DRC':0,'native_unconnected':0,'native_schematic_parity':0,'J1_logical_and_actual_pad_matches':20,'MCU_pin_net_checks':48,'new_CAM_geometry_matches_fresh_source_export':True,'source_CAM_parameter_policy':'Explicit common layer arguments; never reuse historical board-plot drill markers','controller_active_refs':24,'cost_scope':'Additional drill controller board; not included in selected three-board90/111 BOM coverage','boot_register_test':'PASS both prior states; actual boot function source retained, only unused forever-wait main entry removed for host test translation unit','target_build':ver,'critical_open_application_gate':{'signal':'DRV_SLEEP_N','source':'U1pin3 PC14','MCU_limit':'PC13/14/15 limited output load30pF and speed2MHz; must not be used as current sources per manufacturer note','receiver':'DRV8316 nSLEEP pin23; input capacitance30pF specified','total_cable_trace_and_receiver_load':'Not established; cannot certify PC14 push-pull waking output','next_action':'Select normal GPIO route or buffered interface before runnable motor firmware; do not infer electrical release from this netlist pass'},'physical':'NOT_RUN_NO_SAMPLE','powered_test_release':False,'manufacturing_status':'FABRICATION_HOLD','sources':[{'url':'https://www.st.com/resource/en/datasheet/stm32g431cb.pdf','revision':'DS12589 Rev6','pages':[45,60,111,113]},{'url':'https://www.ti.com/lit/ds/symlink/drv8316.pdf','revision':'SLVSF16B RevB','pages':[10]}]})
(ROOT/'controller_pair_checks.json').write_text(json.dumps(c,indent=2)+'\n')
report=H/'A31-drill-interface-review.md';report.write_text('''# A31 电磨控制板与电机板接口核验

状态：FABRICATION_HOLD。没有烧录/加电、线束实测、手柄负载或产品同台验证。

## 已完成的端到端逻辑与 CAD 配对

将已存在的紧凑台架控制板建立完整配对候选：原理图、PCB、项目、精确本地符号库。校正项目实例、封装库标识、网名/NC元数据和MCU数据手册字段；没有改变原电路、走线、焊盘或板形。新鲜KiCad ERC0、物理DRC0、未连接0、原理图/PCB一致性问题0。九层加工图与相同显式参数的新鲜源板导出几何相同，已重新导出钻孔和贴装坐标。

控制板J1与A29电机板J1的20针网表及实际PCB焊盘逐针匹配；MCU的48个封装引脚与网络对应记录在CSV中。RUN请求为MCU13脚PA5→接口10脚→电机板R5/Q1→DRVOFF；睡眠为MCU3脚PC14→接口9脚→DRV8316的nSLEEP；六路PWM和SPI/采样信号也逐针记录。此结果不验证连接器实物型号、面对方向、线束交叉或电平质量。

## 已完成的固件证据

保留电磨实际安全启动函数源码，在主机寄存器模型中检查PA5 RUN低、PC14睡眠低、六路PWM低、PA12充电使能_N高。覆盖全零和先前不同寄存器状态，并检查其他GPIO模式未改变。测试桩使用ST头文件的真实寄存器结构/位定义，替换MMIO指针/核心屏障；仅从测试翻译单元移除不会被调用的无限等待main入口，boot函数内容相同。

Cortex-M4静态镜像重新编译、链接和结构校验通过，保留源码、依赖、ELF/BIN/MAP及构建记录。电机/充电输出未启用，TIM1仍未初始化，FOC对象只编译不链接到该镜像。本轮通过不代表电机能运转。

## 新发现的唤醒驱动条件

PC14属于原厂限制输出组：负载30pF、速度不超过2MHz，并禁止用作电流源。DRV8316的nSLEEP输入电容指标已经是30pF，尚未计入板间线束和走线。故不能直接把当前PC14推挽唤醒输出判定为已合格；需要调整到常规GPIO或缓冲电路，再实测时序/电平。当前安全镜像只将睡眠拉低，此门槛仍阻止可运行控制固件放行。

## 完整成本范围

本轮另列控制板24个位号、每款千台用量24,000个器件位号，全部工厂价格留空。此前90/111仅覆盖电磨电机板、吸尘器板和灯电源板三个所选板的条件候选，并非三款完整整机BOM覆盖率。控制板、充电板、显示/灯阵列、手柄/风机/光源、结构件、装配与测试仍需纳入整机成本。不能用这三个板的覆盖率作为整机可量产或低成本结论。

本包为控制板配对与接口评审增量，当前电机板继续A29，吸尘器CAD继续A29、关断诊断固件A30，灯板继续A24。控制板165×79mm仅为台架紧凑候选，未证明适配原产品壳体。三款功能效果一致和落地级目标仍未完成。

原厂依据：https://www.st.com/resource/en/datasheet/stm32g431cb.pdf 第45页封装图、第60页PC13/14/15限制；https://www.ti.com/lit/ds/symlink/drv8316.pdf 第10页nSLEEP逻辑与输入电容。物理读数继续NOT_RUN。
''',encoding='utf-8')
files=sorted([report]+[p for p in ROOT.rglob('*') if p.is_file() and 'comparison_baseline' not in p.parts and '__pycache__' not in p.parts and p.suffix!='.o' and p.name!='test_boot']);manifest=''.join(sha(p)+'  A31/'+p.relative_to(BASE).as_posix()+'\n' for p in files);zp=H/'Drill-controller-paired-interface-review-A31.zip'
with zipfile.ZipFile(zp,'w',zipfile.ZIP_DEFLATED) as z:
 for p in files:z.write(p,'A31/'+p.relative_to(BASE).as_posix())
 z.writestr('A31/SHA256SUMS.txt',manifest)
with zipfile.ZipFile(zp) as z:
 assert z.testzip() is None
 for ln in z.read('A31/SHA256SUMS.txt').decode().splitlines():
  dig,n=ln.split('  ',1);assert hashlib.sha256(z.read(n)).hexdigest()==dig
(H/'A31-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','native_checks':'Controller ERC0;DRC0;unconnected0;parity0','interface_pin_matches':20,'host_boot_test':'PASS register model only','target_build':'STATIC_PASS_OUTPUTS_OFF','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':'A31_DRILL_CONTROLLER_PAIRED_INTERFACE_NETS_VERIFIED_PC14_LOAD_GATE_OPEN','latest_report':report.name,'latest_machine_report':'../drill_interface_A31/controller_pair_checks.json','latest_manifest':'A31-SHA256SUMS.txt'})
idx['latest_candidate_sources']['drill_control']='../drill_interface_A31/controller_candidate/'+NEW+'.kicad_pcb';idx['drill_controller_cost_scope']={'refs':24,'BOM':'../drill_interface_A31/drill_control_24refs_cost_scope_A31.csv','pricing':'NONE','prior_90_of_111_scope':'Selected motor/vacuum/lamp-power boards, not full products'}
idx['drill_sleep_drive_gate_A31']=c['critical_open_application_gate'];idx['drill_sleep_drive_gate_A31']['powered_test_release']=False;idx['latest_drill_safe_firmware_A31']={'source':'../drill_interface_A31/firmware/固件/drill_target','output_enabled':False,'target_build':'STATIC_PASS','physical_test_release':False};p.write_text(json.dumps(idx,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'package':entry,'controller_checks':{k:c[k] for k in ['native_ERC','native_DRC','native_unconnected','native_schematic_parity','J1_logical_and_actual_pad_matches','MCU_pin_net_checks']}},indent=2))
