from pathlib import Path
import sys,json,csv,re,hashlib,zipfile,subprocess
ROOT=Path(__file__).resolve().parent;BASE=ROOT.parent;H=BASE/'engineering_handoff_local'
sys.path.insert(0,str(BASE/'parity_metadata_closure_A22'))
from align_and_verify import netlist,footprint_rows
from prepare_candidates import forms,sha
board=BASE/'logic_fet_A29/vacuum/vacuum_control_evt_a5_logic_fet.kicad_pcb';sch=board.with_suffix('.kicad_sch');meta=json.loads((BASE/'logic_fet_A29/logic_fet_checks.json').read_text())['vacuum'];assert sha(board)==meta['board_sha256'] and sha(sch)==meta['schematic_sha256']
_,nets=netlist(board.parent/'netlist.xml');pn={k[1]:v for k,v in nets.items() if k[0]=='U5'}
fp=next(b for _,_,b,r,_ in footprint_rows(board.read_text()) if r=='U5')
actual={re.match(r'\(pad "([^\"]+)"',b)[1]:re.search(r'\(net "([^\"]+)"',b)[1] for _,_,b in forms(fp,'pad')};assert pn=={k:v.replace('{slash}','/') for k,v in actual.items()}
assert pn['15']=='/FAN_PWM_CMD' and pn['17']=='/FAN_PWR_EN'
default={1:'PB7/PB8',2:'PB9/PC14',3:'PC15',4:'VDD/VDDA',5:'VSS/VSSA',6:'NRST',7:'PA0',8:'PA1',9:'PA2',10:'PA3',11:'PA4',12:'PA5',13:'PA6',14:'PA7',15:'PB0/PB1/PB2/PA8',16:'PA11 (optional PA9 remap)',17:'PA12 (optional PA10 remap)',18:'PA13/SWDIO',19:'PA14/BOOT0 and PA15',20:'PB3/PB4/PB5/PB6'}
rows=[{'Ref':'U5','PackagePin':i,'ManufacturerFunction':default[i],'SchematicNet':pn[str(i)],'PCBPadNet':actual[str(i)],'FirmwareUse':'PA12 GPIO output low; PA12_RMP cleared after PA10/PA12 staged low' if i==17 else 'PA8 AF2 TIM1_CH1; power stays off' if i==15 else 'See firmware source; unused bonded GPIOs remain at startup settings','Qualification':'PIN_NET_AND_FUNCTION_REVIEW_NOT_PHYSICAL_TEST'} for i in range(1,21)]
with (ROOT/'U5_manufacturer_pin_net_review.csv').open('w',encoding='utf-8-sig',newline='') as f:w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
t=ROOT/'firmware/固件/vacuum_target';ver=json.loads((t/'build/build_verification.json').read_text());assert ver['status']=='LINKED_IMAGE_STATIC_CHECK_PASS' and not ver['physical_hardware_validation'] and not ver['fan_power_output_enabled']
assert sha(t/'build/vacuum_control_g030f6.bin')==ver['sha256_bin'];assert sha(t/'build/vacuum_control_g030f6.elf')==ver['sha256_elf']
# Reconfirm target image evidence and disassemble the freshly rebuilt image.
p=subprocess.run(['arm-none-eabi-objdump','-d',str(t/'build/vacuum_control_g030f6.elf')],capture_output=True,text=True,check=True);(ROOT/'target_disassembly.txt').write_text(p.stdout)
p=subprocess.run(['cc','-std=c11','-Wall','-Wextra','-Werror',str(t/'test/test_vacuum_output_lock.c'),str(t/'src/vacuum_output_lock.c'),'-o',str(ROOT/'host_test/test_output_lock')],capture_output=True,text=True,check=True);p=subprocess.run([str(ROOT/'host_test/test_output_lock')],capture_output=True,text=True,check=True);(ROOT/'output_lock_test.log').write_text(p.stdout)
# The threshold is a design envelope only, leaving switching/temperature tests open.
rgmax=680*1.01;pdmin=100000*.99;gate_min=(3.0-.4)*pdmin/(pdmin+rgmax)
c={'CAD_board_sha256':sha(board),'CAD_schematic_sha256':sha(sch),'CAD_changed':False,'U5_pin_net_match_count':20,'NC_name_normalization':'4 NC nets contain KiCad {slash} escape in PCB and literal slash in XML; semantic names normalized for comparison, raw labels retained in CSV','physical_pin17_default':'PA12','physical_pin17_net':'FAN_PWR_EN','previous_firmware_pin':'PA10 without PA12_RMP enabling','corrected_firmware_pin':'PA12 with explicit PA12_RMP clear; both logical candidates first staged low','PA8_PWM_AF':'AF2 TIM1_CH1','host_register_test':'PASS:16 actual board-function scenarios covering prior remap0/1, timer0/1, forceoff,forcedisabled,power-on rejection,nonzero-duty rejection','old_firmware_negative_control':'FAIL as expected at physical pin17 output-mode assertion','output_lock_test':'PASS','host_test_does_not_model':['GPIO voltage or BSRR hardware side effects','Power FET/fan transient or analog behavior','Actual MMIO instruction timing or MCU fault modes'],'target_build':ver,'electrical_review':{'STM32G030_table50_CMOS_Voh_min':'VDDIO1-0.4V at |Iio|6mA,VDDIO1>=2.7V','series_peak_A_at3p6V_680ohm_1pct':3.6/(680*.99),'conditional_min_rail_V':3.0,'ideal_static_gate_min_V_680_1pct_100k_1pct_no_gate_leakage':gate_min,'BSS138BK_characterized_Rds_gate_V':2.5,'margin_V_ideal_only':gate_min-2.5,'not_proven':'Actual rail envelope, gate leakage/dynamic droop and transistor temperature; CMOS static margin calculation cannot prove physical switching.'},'sources':[{'url':'https://www.st.com/resource/en/datasheet/stm32g030f6.pdf','revision':'DS12991 Rev6 June2025','pages':[29,33,34,35,64],'read_method':'Official web PDF text; no new local PDF hash'},{'path':'firmware/固件/lamp_target/Device/Include/stm32g030xx.h','sha256':sha(ROOT/'firmware/固件/lamp_target/Device/Include/stm32g030xx.h'),'use':'Pinned manufacturer register structures and PA12_RMP mask; host core stubs documented'}],'manufacturing_status':'FABRICATION_HOLD','powered_test_release':False,'physical_validation':'NOT_RUN_NO_SAMPLE','product_equivalence':'NOT_VERIFIED'}
(ROOT/'mcu_pinmap_checks.json').write_text(json.dumps(c,indent=2)+'\n')
report=H/'A30-MCU-pinmap-and-firmware-review.md';report.write_text('''# A30 吸尘器电源使能 MCU 映射与关断固件修订

状态：FABRICATION_HOLD。没有烧录、加电或参考机对比，不放行通电打样。

## 发现的实际功能问题

A29吸尘器U5为STM32G030F6P6，FAN_PWR_EN接TSSOP20第17脚。原厂DS12991 Rev6引脚表和注4说明该脚默认PA12，只有SYSCFG_CFGR1重映射后才是PA10。此前诊断固件使用PA10，却没有启用重映射。因此原关断路径未主动控制连接Q4的实际引脚，后来解除运行锁也会使使能控制失效。栅极外部下拉仍存在，但不能代替正确的主动控制。

## 已落实的修改

本版采用默认PA12控制第17脚，不改板上网络。在forceoff和forcedisabled的公共路径中，先开启GPIOA/SYSCFG时钟，将PA10和PA12逻辑输出都预置低，再清除PA12_RMP以选择默认PA12。这样在先前存在重映射的情况下也先将当前物理驱动预置低。保留其他重映射位、PWM关断和输出锁。PA8仍用AF2的TIM1_CH1；没有开启风机功能输出。

本地U5的20个封装引脚逐项记录原厂功能、原理图网络与PCB焊盘网络，网络在规范化KiCad的斜杠转义后全部匹配（四个NC网络在PCB中使用{slash}）；第15脚FAN_PWM_CMD和第17脚FAN_PWR_EN与固件分别对应PA8和PA12。部分封装脚同时绑定多个GPIO，CSV保留该事实，不能用符号中的单一别名替代原厂映射。

## 验证结果及范围

主机测试直接编译修改后的vacuum_board.c，使用已固定的ST头文件寄存器结构/位定义，将MMIO基址换为主机寄存器对象，核心屏障与中断调用为测试桩。覆盖默认/先前PA10重映射、定时器时钟开/关、两种关断函数及电源/PWM非零请求被输出锁拒绝，共16种调用场景；全部通过。将旧源代码放进同一测试，第17脚必须配置为输出的断言失败，证明测试能发现原问题。

独立输出锁回归通过。实际ARM Cortex-M0+镜像重新编译、链接及镜像结构校验通过：FLASH3828字节、RAM1352字节，SHA256见机器报告。保留ELF/BIN/MAP、构建日志和新鲜反汇编。

这些是主机寄存器写入和静态目标镜像证据，不证明GPIO实际电压、BSRR硬件副作用、关断时间、电源MOSFET或风机已实测。依赖目录lamp_target仅作为已固定CMSIS/startup/linker构建依赖，不能视为本轮灯具发布。

## 电平条件与未完成项

原厂表50给出G030在VDDIO1≥2.7V、6mA条件下CMOS输出高电平≥VDDIO1-0.4V。680Ω1%在假定3.6V最高轨下栅极充放电初始峰值算术≤5.35mA；在设计最低轨3.0V、100kΩ1%下拉、忽略栅极漏电和动态时，栅极高电平算术约2.582V。这只给出静态设计包络，仍需核实实际最低轨、漏电、温度和动态电压。

仍需实际第17脚在冷启动、复位、故障、停止时的波形，以及Q4/Q3开启/关闭、风机PWM上拉/极性/脉宽与转速等测试。电磨控制板MCU到外部DRV_RUN_REQ的端到端电平审查、驱动阈值/热/EMC/DFM及原产品同台对比仍未闭环。三款功能效果一致与量产成本达标未确认。

本轮CAD和BOM未修改，当前电磨与吸尘器CAD沿用A29、灯板沿用A24；三款90/111为条件候选料号，不能视为90个器件已应用验证。本包是新的吸尘器关断诊断固件候选，应撤销旧PA10无重映射诊断镜像的当前使用资格。

原厂依据：https://www.st.com/resource/en/datasheet/stm32g030f6.pdf ，第29页TSSOP20引脚图、第33–34页引脚表与重映射注释、第35页PA8 AF2、第64页输出电平表。URL和引用页已记录，无原厂PDF再分发。
''',encoding='utf-8')
files=sorted([report]+[p for p in ROOT.rglob('*') if p.is_file() and '__pycache__' not in p.parts and p.name not in ['test_fixed','test_original','test_output_lock']]);manifest=''.join(sha(p)+'  A30/'+p.relative_to(BASE).as_posix()+'\n' for p in files);zp=H/'Vacuum-MCU-pin17-PA12-firmware-A30.zip'
with zipfile.ZipFile(zp,'w',zipfile.ZIP_DEFLATED) as z:
 for p in files:z.write(p,'A30/'+p.relative_to(BASE).as_posix())
 z.writestr('A30/SHA256SUMS.txt',manifest)
with zipfile.ZipFile(zp) as z:
 assert z.testzip() is None
 for ln in z.read('A30/SHA256SUMS.txt').decode().splitlines():
  dig,n=ln.split('  ',1);assert hashlib.sha256(z.read(n)).hexdigest()==dig
(H/'A30-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','firmware':'U5 pin17 PA12 diagnostic output-off candidate','host_register_test':'PASS + original negative control fails','target_image':'STATIC_BUILD_PASS','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':'A30_VACUUM_PIN17_PA12_FIRMWARE_CORRECTED_PHYSICAL_NOT_RUN','latest_report':report.name,'latest_machine_report':'../mcu_pinmap_A30/mcu_pinmap_checks.json','latest_manifest':'A30-SHA256SUMS.txt'})
idx['latest_vacuum_diagnostic_firmware']={'source':'../mcu_pinmap_A30/firmware/固件/vacuum_target','binary':'../mcu_pinmap_A30/firmware/固件/vacuum_target/build/vacuum_control_g030f6.bin','sha256':ver['sha256_bin'],'output_enabled':False,'physical_test_release':False,'board_pair':'A29 vacuum_control_evt_a5_logic_fet'}
idx['manufacturing_exclusions']['vacuum_previous_PA10_diagnostic_firmware']='Original vacuum_vref_calibration_a0 runtime operated PA10 without PA12 remap, but physical U5pin17 defaults PA12. Superseded by A30; neither flashed nor physically qualified.'
idx['vacuum_MCU_pin17_mapping_gate']={'CAD_pin_net_match':'20/20 U5','firmware_correction':'PA12 selected after both PA10/PA12 are staged low','host_test':'PASS; original source fails physical-pad assertion','physical_pin_voltage_waveform':'NOT_RUN','powered_test_release':False};p.write_text(json.dumps(idx,ensure_ascii=False,indent=2)+'\n');print(json.dumps({'package':entry,'image':ver,'gate_ideal_V':gate_min},indent=2))
